Context.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\apidoc\models;
  8. use phpDocumentor\Reflection\FileReflector;
  9. use yii\base\Component;
  10. /**
  11. *
  12. * @author Carsten Brandt <mail@cebe.cc>
  13. * @since 2.0
  14. */
  15. class Context extends Component
  16. {
  17. /**
  18. * @var array list of php files that have been added to this context.
  19. */
  20. public $files = [];
  21. /**
  22. * @var ClassDoc[]
  23. */
  24. public $classes = [];
  25. /**
  26. * @var InterfaceDoc[]
  27. */
  28. public $interfaces = [];
  29. /**
  30. * @var TraitDoc[]
  31. */
  32. public $traits = [];
  33. /**
  34. * @var array
  35. */
  36. public $errors = [];
  37. /**
  38. * @var array
  39. * @since 2.0.6
  40. */
  41. public $warnings = [];
  42. /**
  43. * Returning TypeDoc for a type given
  44. * @param string $type
  45. * @return null|ClassDoc|InterfaceDoc|TraitDoc
  46. */
  47. public function getType($type)
  48. {
  49. $type = ltrim($type, '\\');
  50. if (isset($this->classes[$type])) {
  51. return $this->classes[$type];
  52. } elseif (isset($this->interfaces[$type])) {
  53. return $this->interfaces[$type];
  54. } elseif (isset($this->traits[$type])) {
  55. return $this->traits[$type];
  56. }
  57. return null;
  58. }
  59. /**
  60. * Adds file to context
  61. * @param string $fileName
  62. */
  63. public function addFile($fileName)
  64. {
  65. $this->files[$fileName] = sha1_file($fileName);
  66. $reflection = new FileReflector($fileName, true);
  67. $reflection->process();
  68. foreach ($reflection->getClasses() as $class) {
  69. $class = new ClassDoc($class, $this, ['sourceFile' => $fileName]);
  70. $this->classes[$class->name] = $class;
  71. }
  72. foreach ($reflection->getInterfaces() as $interface) {
  73. $interface = new InterfaceDoc($interface, $this, ['sourceFile' => $fileName]);
  74. $this->interfaces[$interface->name] = $interface;
  75. }
  76. foreach ($reflection->getTraits() as $trait) {
  77. $trait = new TraitDoc($trait, $this, ['sourceFile' => $fileName]);
  78. $this->traits[$trait->name] = $trait;
  79. }
  80. }
  81. /**
  82. * Updates references
  83. */
  84. public function updateReferences()
  85. {
  86. // update all subclass references
  87. foreach ($this->classes as $class) {
  88. $className = $class->name;
  89. while (isset($this->classes[$class->parentClass])) {
  90. $class = $this->classes[$class->parentClass];
  91. $class->subclasses[] = $className;
  92. }
  93. }
  94. // update interfaces of subclasses
  95. foreach ($this->classes as $class) {
  96. $this->updateSubclassInterfacesTraits($class);
  97. }
  98. // update implementedBy and usedBy for interfaces and traits
  99. foreach ($this->classes as $class) {
  100. foreach ($class->traits as $trait) {
  101. if (isset($this->traits[$trait])) {
  102. $trait = $this->traits[$trait];
  103. $trait->usedBy[] = $class->name;
  104. $class->properties = array_merge($trait->properties, $class->properties);
  105. $class->methods = array_merge($trait->methods, $class->methods);
  106. }
  107. }
  108. foreach ($class->interfaces as $interface) {
  109. if (isset($this->interfaces[$interface])) {
  110. $this->interfaces[$interface]->implementedBy[] = $class->name;
  111. if ($class->isAbstract) {
  112. // add not implemented interface methods
  113. foreach ($this->interfaces[$interface]->methods as $method) {
  114. if (!isset($class->methods[$method->name])) {
  115. $class->methods[$method->name] = $method;
  116. }
  117. }
  118. }
  119. }
  120. }
  121. }
  122. foreach ($this->interfaces as $interface) {
  123. foreach ($interface->parentInterfaces as $pInterface) {
  124. if (isset($this->interfaces[$pInterface])) {
  125. $this->interfaces[$pInterface]->implementedBy[] = $interface->name;
  126. }
  127. }
  128. }
  129. // inherit docs
  130. foreach ($this->classes as $class) {
  131. $this->inheritDocs($class);
  132. }
  133. // inherit properties, methods, contants and events to subclasses
  134. foreach ($this->classes as $class) {
  135. $this->updateSubclassInheritance($class);
  136. }
  137. foreach ($this->interfaces as $interface) {
  138. $this->updateSubInterfaceInheritance($interface);
  139. }
  140. // add properties from getters and setters
  141. foreach ($this->classes as $class) {
  142. $this->handlePropertyFeature($class);
  143. }
  144. // TODO reference exceptions to methods where they are thrown
  145. }
  146. /**
  147. * Add implemented interfaces and used traits to subclasses
  148. * @param ClassDoc $class
  149. */
  150. protected function updateSubclassInterfacesTraits($class)
  151. {
  152. foreach ($class->subclasses as $subclass) {
  153. $subclass = $this->classes[$subclass];
  154. $subclass->interfaces = array_unique(array_merge($subclass->interfaces, $class->interfaces));
  155. $subclass->traits = array_unique(array_merge($subclass->traits, $class->traits));
  156. $this->updateSubclassInterfacesTraits($subclass);
  157. }
  158. }
  159. /**
  160. * Add implemented interfaces and used traits to subclasses
  161. * @param ClassDoc $class
  162. */
  163. protected function updateSubclassInheritance($class)
  164. {
  165. foreach ($class->subclasses as $subclass) {
  166. $subclass = $this->classes[$subclass];
  167. $subclass->events = array_merge($class->events, $subclass->events);
  168. $subclass->constants = array_merge($class->constants, $subclass->constants);
  169. $subclass->properties = array_merge($class->properties, $subclass->properties);
  170. $subclass->methods = array_merge($class->methods, $subclass->methods);
  171. $this->updateSubclassInheritance($subclass);
  172. }
  173. }
  174. /**
  175. * Add methods to subinterfaces
  176. * @param InterfaceDoc $class
  177. */
  178. protected function updateSubInterfaceInheritance($interface)
  179. {
  180. foreach ($interface->implementedBy as $subInterface) {
  181. if (isset($this->interfaces[$subInterface])) {
  182. $subInterface = $this->interfaces[$subInterface];
  183. $subInterface->methods = array_merge($interface->methods, $subInterface->methods);
  184. $this->updateSubInterfaceInheritance($subInterface);
  185. }
  186. }
  187. }
  188. /**
  189. * Inhertit docsblocks using `@inheritDoc` tag.
  190. * @param ClassDoc $class
  191. * @see http://phpdoc.org/docs/latest/guides/inheritance.html
  192. */
  193. protected function inheritDocs($class)
  194. {
  195. // inherit for properties
  196. foreach ($class->properties as $p) {
  197. if ($p->hasTag('inheritdoc') && ($inheritTag = $p->getFirstTag('inheritdoc')) !== null) {
  198. $inheritedProperty = $this->inheritPropertyRecursive($p, $class);
  199. if (!$inheritedProperty) {
  200. $this->errors[] = [
  201. 'line' => $p->startLine,
  202. 'file' => $class->sourceFile,
  203. 'message' => "Method {$p->name} has no parent to inherit from in {$class->name}.",
  204. ];
  205. continue;
  206. }
  207. // set all properties that are empty.
  208. foreach (['shortDescription', 'type', 'types'] as $property) {
  209. if (empty($p->$property) || is_string($p->$property) && trim($p->$property) === '') {
  210. $p->$property = $inheritedProperty->$property;
  211. }
  212. }
  213. // descriptions will be concatenated.
  214. $p->description = trim($p->description) . "\n\n"
  215. . trim($inheritedProperty->description) . "\n\n"
  216. . $inheritTag->getContent();
  217. $p->removeTag('inheritdoc');
  218. }
  219. }
  220. // inherit for methods
  221. foreach ($class->methods as $m) {
  222. if ($m->hasTag('inheritdoc') && ($inheritTag = $m->getFirstTag('inheritdoc')) !== null) {
  223. $inheritedMethod = $this->inheritMethodRecursive($m, $class);
  224. if (!$inheritedMethod) {
  225. $this->errors[] = [
  226. 'line' => $m->startLine,
  227. 'file' => $class->sourceFile,
  228. 'message' => "Method {$m->name} has no parent to inherit from in {$class->name}.",
  229. ];
  230. continue;
  231. }
  232. // set all properties that are empty.
  233. foreach (['shortDescription', 'return', 'returnType', 'returnTypes', 'exceptions'] as $property) {
  234. if (empty($m->$property) || is_string($m->$property) && trim($m->$property) === '') {
  235. $m->$property = $inheritedMethod->$property;
  236. }
  237. }
  238. // descriptions will be concatenated.
  239. $m->description = trim($m->description) . "\n\n"
  240. . trim($inheritedMethod->description) . "\n\n"
  241. . $inheritTag->getContent();
  242. foreach ($m->params as $i => $param) {
  243. if (!isset($inheritedMethod->params[$i])) {
  244. $this->errors[] = [
  245. 'line' => $m->startLine,
  246. 'file' => $class->sourceFile,
  247. 'message' => "Method param $i does not exist in parent method, @inheritdoc not possible in {$m->name} in {$class->name}.",
  248. ];
  249. continue;
  250. }
  251. if (empty($param->description) || trim($param->description) === '') {
  252. $param->description = $inheritedMethod->params[$i]->description;
  253. }
  254. if (empty($param->type) || trim($param->type) === '') {
  255. $param->type = $inheritedMethod->params[$i]->type;
  256. }
  257. if (empty($param->types)) {
  258. $param->types = $inheritedMethod->params[$i]->types;
  259. }
  260. }
  261. $m->removeTag('inheritdoc');
  262. }
  263. }
  264. }
  265. /**
  266. * @param MethodDoc $method
  267. * @param ClassDoc $class
  268. * @return mixed
  269. */
  270. private function inheritMethodRecursive($method, $class)
  271. {
  272. $inheritanceCandidates = array_merge(
  273. $this->getParents($class),
  274. $this->getInterfaces($class)
  275. );
  276. $methods = [];
  277. foreach($inheritanceCandidates as $candidate) {
  278. if (isset($candidate->methods[$method->name])) {
  279. $cmethod = $candidate->methods[$method->name];
  280. if ($cmethod->hasTag('inheritdoc')) {
  281. $this->inheritDocs($candidate);
  282. }
  283. $methods[] = $cmethod;
  284. }
  285. }
  286. return reset($methods);
  287. }
  288. /**
  289. * @param PropertyDoc $method
  290. * @param ClassDoc $class
  291. * @return mixed
  292. */
  293. private function inheritPropertyRecursive($method, $class)
  294. {
  295. $inheritanceCandidates = array_merge(
  296. $this->getParents($class),
  297. $this->getInterfaces($class)
  298. );
  299. $properties = [];
  300. foreach($inheritanceCandidates as $candidate) {
  301. if (isset($candidate->properties[$method->name])) {
  302. $cproperty = $candidate->properties[$method->name];
  303. if ($cproperty->hasTag('inheritdoc')) {
  304. $this->inheritDocs($candidate);
  305. }
  306. $properties[] = $cproperty;
  307. }
  308. }
  309. return reset($properties);
  310. }
  311. /**
  312. * @param ClassDoc $class
  313. * @return array
  314. */
  315. private function getParents($class)
  316. {
  317. if ($class->parentClass === null || !isset($this->classes[$class->parentClass])) {
  318. return [];
  319. }
  320. return array_merge([$this->classes[$class->parentClass]], $this->getParents($this->classes[$class->parentClass]));
  321. }
  322. /**
  323. * @param ClassDoc $class
  324. * @return array
  325. */
  326. private function getInterfaces($class)
  327. {
  328. $interfaces = [];
  329. foreach($class->interfaces as $interface) {
  330. if (isset($this->interfaces[$interface])) {
  331. $interfaces[] = $this->interfaces[$interface];
  332. }
  333. }
  334. return $interfaces;
  335. }
  336. /**
  337. * Add properties for getters and setters if class is subclass of [[\yii\base\Object]].
  338. * @param ClassDoc $class
  339. */
  340. protected function handlePropertyFeature($class)
  341. {
  342. if (!$this->isSubclassOf($class, 'yii\base\Object')) {
  343. return;
  344. }
  345. foreach ($class->getPublicMethods() as $name => $method) {
  346. if ($method->isStatic) {
  347. continue;
  348. }
  349. if (!strncmp($name, 'get', 3) && strlen($name) > 3 && $this->hasNonOptionalParams($method)) {
  350. $propertyName = '$' . lcfirst(substr($method->name, 3));
  351. if (isset($class->properties[$propertyName])) {
  352. $property = $class->properties[$propertyName];
  353. if ($property->getter === null && $property->setter === null) {
  354. $this->errors[] = [
  355. 'line' => $property->startLine,
  356. 'file' => $class->sourceFile,
  357. 'message' => "Property $propertyName conflicts with a defined getter {$method->name} in {$class->name}.",
  358. ];
  359. }
  360. $property->getter = $method;
  361. } else {
  362. $class->properties[$propertyName] = new PropertyDoc(null, $this, [
  363. 'name' => $propertyName,
  364. 'definedBy' => $method->definedBy,
  365. 'sourceFile' => $class->sourceFile,
  366. 'visibility' => 'public',
  367. 'isStatic' => false,
  368. 'type' => $method->returnType,
  369. 'types' => $method->returnTypes,
  370. 'shortDescription' => BaseDoc::extractFirstSentence($method->return),
  371. 'description' => $method->return,
  372. 'getter' => $method
  373. // TODO set default value
  374. ]);
  375. }
  376. }
  377. if (!strncmp($name, 'set', 3) && strlen($name) > 3 && $this->hasNonOptionalParams($method, 1)) {
  378. $propertyName = '$' . lcfirst(substr($method->name, 3));
  379. if (isset($class->properties[$propertyName])) {
  380. $property = $class->properties[$propertyName];
  381. if ($property->getter === null && $property->setter === null) {
  382. $this->errors[] = [
  383. 'line' => $property->startLine,
  384. 'file' => $class->sourceFile,
  385. 'message' => "Property $propertyName conflicts with a defined setter {$method->name} in {$class->name}.",
  386. ];
  387. }
  388. $property->setter = $method;
  389. } else {
  390. $param = $this->getFirstNotOptionalParameter($method);
  391. $class->properties[$propertyName] = new PropertyDoc(null, $this, [
  392. 'name' => $propertyName,
  393. 'definedBy' => $method->definedBy,
  394. 'sourceFile' => $class->sourceFile,
  395. 'visibility' => 'public',
  396. 'isStatic' => false,
  397. 'type' => $param->type,
  398. 'types' => $param->types,
  399. 'shortDescription' => BaseDoc::extractFirstSentence($param->description),
  400. 'description' => $param->description,
  401. 'setter' => $method
  402. ]);
  403. }
  404. }
  405. }
  406. }
  407. /**
  408. * Check whether a method has `$number` non-optional parameters.
  409. * @param MethodDoc $method
  410. * @param int $number number of not optional parameters
  411. * @return bool
  412. */
  413. private function hasNonOptionalParams($method, $number = 0)
  414. {
  415. $count = 0;
  416. foreach ($method->params as $param) {
  417. if (!$param->isOptional) {
  418. $count++;
  419. }
  420. }
  421. return $count == $number;
  422. }
  423. /**
  424. * @param MethodDoc $method
  425. * @return ParamDoc
  426. */
  427. private function getFirstNotOptionalParameter($method)
  428. {
  429. foreach ($method->params as $param) {
  430. if (!$param->isOptional) {
  431. return $param;
  432. }
  433. }
  434. return null;
  435. }
  436. /**
  437. * @param ClassDoc $classA
  438. * @param ClassDoc|string $classB
  439. * @return bool
  440. */
  441. protected function isSubclassOf($classA, $classB)
  442. {
  443. if (is_object($classB)) {
  444. $classB = $classB->name;
  445. }
  446. if ($classA->name == $classB) {
  447. return true;
  448. }
  449. while ($classA->parentClass !== null && isset($this->classes[$classA->parentClass])) {
  450. $classA = $this->classes[$classA->parentClass];
  451. if ($classA->name == $classB) {
  452. return true;
  453. }
  454. }
  455. return false;
  456. }
  457. }