[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class DivinerAtomController extends DivinerController { 4 5 private $bookName; 6 private $atomType; 7 private $atomName; 8 private $atomContext; 9 private $atomIndex; 10 11 public function shouldAllowPublic() { 12 return true; 13 } 14 15 public function willProcessRequest(array $data) { 16 $this->bookName = $data['book']; 17 $this->atomType = $data['type']; 18 $this->atomName = $data['name']; 19 $this->atomContext = nonempty(idx($data, 'context'), null); 20 $this->atomIndex = nonempty(idx($data, 'index'), null); 21 } 22 23 public function processRequest() { 24 $request = $this->getRequest(); 25 $viewer = $request->getUser(); 26 27 require_celerity_resource('diviner-shared-css'); 28 29 $book = id(new DivinerBookQuery()) 30 ->setViewer($viewer) 31 ->withNames(array($this->bookName)) 32 ->executeOne(); 33 34 if (!$book) { 35 return new Aphront404Response(); 36 } 37 38 // TODO: This query won't load ghosts, because they'll fail `needAtoms()`. 39 // Instead, we might want to load ghosts and render a message like 40 // "this thing existed in an older version, but no longer does", especially 41 // if we add content like comments. 42 43 $symbol = id(new DivinerAtomQuery()) 44 ->setViewer($viewer) 45 ->withBookPHIDs(array($book->getPHID())) 46 ->withTypes(array($this->atomType)) 47 ->withNames(array($this->atomName)) 48 ->withContexts(array($this->atomContext)) 49 ->withIndexes(array($this->atomIndex)) 50 ->needAtoms(true) 51 ->needExtends(true) 52 ->needChildren(true) 53 ->executeOne(); 54 55 if (!$symbol) { 56 return new Aphront404Response(); 57 } 58 59 $atom = $symbol->getAtom(); 60 $crumbs = $this->buildApplicationCrumbs(); 61 62 $crumbs->addTextCrumb( 63 $book->getShortTitle(), 64 '/book/'.$book->getName().'/'); 65 66 $atom_short_title = $atom->getDocblockMetaValue( 67 'short', 68 $symbol->getTitle()); 69 70 $crumbs->addTextCrumb($atom_short_title); 71 72 $header = id(new PHUIHeaderView()) 73 ->setHeader($this->renderFullSignature($symbol)) 74 ->addTag( 75 id(new PHUITagView()) 76 ->setType(PHUITagView::TYPE_STATE) 77 ->setBackgroundColor(PHUITagView::COLOR_BLUE) 78 ->setName(DivinerAtom::getAtomTypeNameString($atom->getType()))); 79 80 $properties = id(new PHUIPropertyListView()); 81 82 $group = $atom->getProperty('group'); 83 if ($group) { 84 $group_name = $book->getGroupName($group); 85 } else { 86 $group_name = null; 87 } 88 89 $this->buildDefined($properties, $symbol); 90 $this->buildExtendsAndImplements($properties, $symbol); 91 92 $warnings = $atom->getWarnings(); 93 if ($warnings) { 94 $warnings = id(new AphrontErrorView()) 95 ->setErrors($warnings) 96 ->setTitle(pht('Documentation Warnings')) 97 ->setSeverity(AphrontErrorView::SEVERITY_WARNING); 98 } 99 100 $methods = $this->composeMethods($symbol); 101 102 $field = 'default'; 103 $engine = id(new PhabricatorMarkupEngine()) 104 ->setViewer($viewer) 105 ->addObject($symbol, $field); 106 foreach ($methods as $method) { 107 foreach ($method['atoms'] as $matom) { 108 $engine->addObject($matom, $field); 109 } 110 } 111 $engine->process(); 112 113 $content = $this->renderDocumentationText($symbol, $engine); 114 115 $toc = $engine->getEngineMetadata( 116 $symbol, 117 $field, 118 PhutilRemarkupHeaderBlockRule::KEY_HEADER_TOC, 119 array()); 120 121 $document = id(new PHUIDocumentView()) 122 ->setBook($book->getTitle(), $group_name) 123 ->setHeader($header) 124 ->addClass('diviner-view') 125 ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS) 126 ->appendChild($properties) 127 ->appendChild($warnings) 128 ->appendChild($content); 129 130 $document->appendChild($this->buildParametersAndReturn(array($symbol))); 131 132 if ($methods) { 133 $tasks = $this->composeTasks($symbol); 134 135 if ($tasks) { 136 $methods_by_task = igroup($methods, 'task'); 137 138 // Add phantom tasks for methods which have a "@task" name that isn't 139 // documented anywhere, or methods that have no "@task" name. 140 foreach ($methods_by_task as $task => $ignored) { 141 if (empty($tasks[$task])) { 142 $tasks[$task] = array( 143 'name' => $task, 144 'title' => $task ? $task : pht('Other Methods'), 145 'defined' => $symbol, 146 ); 147 } 148 } 149 150 $section = id(new DivinerSectionView()) 151 ->setHeader(pht('Tasks')); 152 153 foreach ($tasks as $spec) { 154 $section->addContent( 155 id(new PHUIHeaderView()) 156 ->setNoBackground(true) 157 ->setHeader($spec['title'])); 158 159 $task_methods = idx($methods_by_task, $spec['name'], array()); 160 $inner_box = id(new PHUIBoxView()) 161 ->addPadding(PHUI::PADDING_LARGE_LEFT) 162 ->addPadding(PHUI::PADDING_LARGE_RIGHT) 163 ->addPadding(PHUI::PADDING_LARGE_BOTTOM); 164 165 $box_content = array(); 166 if ($task_methods) { 167 $list_items = array(); 168 foreach ($task_methods as $task_method) { 169 $atom = last($task_method['atoms']); 170 171 $item = $this->renderFullSignature($atom, true); 172 173 if (strlen($atom->getSummary())) { 174 $item = array( 175 $item, 176 " \xE2\x80\x94 ", 177 $atom->getSummary(), 178 ); 179 } 180 181 $list_items[] = phutil_tag('li', array(), $item); 182 } 183 184 $box_content[] = phutil_tag( 185 'ul', 186 array( 187 'class' => 'diviner-list', 188 ), 189 $list_items); 190 } else { 191 $no_methods = pht('No methods for this task.'); 192 $box_content = phutil_tag('em', array(), $no_methods); 193 } 194 195 $inner_box->appendChild($box_content); 196 $section->addContent($inner_box); 197 } 198 $document->appendChild($section); 199 } 200 201 $section = id(new DivinerSectionView()) 202 ->setHeader(pht('Methods')); 203 204 foreach ($methods as $spec) { 205 $matom = last($spec['atoms']); 206 $method_header = id(new PHUIHeaderView()) 207 ->setNoBackground(true); 208 209 $inherited = $spec['inherited']; 210 if ($inherited) { 211 $method_header->addTag( 212 id(new PHUITagView()) 213 ->setType(PHUITagView::TYPE_STATE) 214 ->setBackgroundColor(PHUITagView::COLOR_GREY) 215 ->setName(pht('Inherited'))); 216 } 217 218 $method_header->setHeader($this->renderFullSignature($matom)); 219 220 $section->addContent( 221 array( 222 $method_header, 223 $this->renderMethodDocumentationText($symbol, $spec, $engine), 224 $this->buildParametersAndReturn($spec['atoms']), 225 )); 226 } 227 $document->appendChild($section); 228 } 229 230 if ($toc) { 231 $side = new PHUIListView(); 232 $side->addMenuItem( 233 id(new PHUIListItemView()) 234 ->setName(pht('Contents')) 235 ->setType(PHUIListItemView::TYPE_LABEL)); 236 foreach ($toc as $key => $entry) { 237 $side->addMenuItem( 238 id(new PHUIListItemView()) 239 ->setName($entry[1]) 240 ->setHref('#'.$key)); 241 } 242 243 $document->setSideNav($side, PHUIDocumentView::NAV_TOP); 244 } 245 246 return $this->buildApplicationPage( 247 array( 248 $crumbs, 249 $document, 250 ), 251 array( 252 'title' => $symbol->getTitle(), 253 )); 254 } 255 256 private function buildExtendsAndImplements( 257 PHUIPropertyListView $view, 258 DivinerLiveSymbol $symbol) { 259 260 $lineage = $this->getExtendsLineage($symbol); 261 if ($lineage) { 262 $tags = array(); 263 foreach ($lineage as $item) { 264 $tags[] = $this->renderAtomTag($item); 265 } 266 267 $caret = phutil_tag('span', array('class' => 'caret-right msl msr')); 268 $tags = phutil_implode_html($caret, $tags); 269 $view->addProperty(pht('Extends'), $tags); 270 } 271 272 $implements = $this->getImplementsLineage($symbol); 273 if ($implements) { 274 $items = array(); 275 foreach ($implements as $spec) { 276 $via = $spec['via']; 277 $iface = $spec['interface']; 278 if ($via == $symbol) { 279 $items[] = $this->renderAtomTag($iface); 280 } else { 281 $items[] = array( 282 $this->renderAtomTag($iface), 283 " \xE2\x97\x80 ", 284 $this->renderAtomTag($via), 285 ); 286 } 287 } 288 289 $view->addProperty( 290 pht('Implements'), 291 phutil_implode_html(phutil_tag('br'), $items)); 292 } 293 294 } 295 296 private function renderAtomTag(DivinerLiveSymbol $symbol) { 297 return id(new PHUITagView()) 298 ->setType(PHUITagView::TYPE_OBJECT) 299 ->setName($symbol->getName()) 300 ->setHref($symbol->getURI()); 301 } 302 303 private function getExtendsLineage(DivinerLiveSymbol $symbol) { 304 foreach ($symbol->getExtends() as $extends) { 305 if ($extends->getType() == 'class') { 306 $lineage = $this->getExtendsLineage($extends); 307 $lineage[] = $extends; 308 return $lineage; 309 } 310 } 311 return array(); 312 } 313 314 private function getImplementsLineage(DivinerLiveSymbol $symbol) { 315 $implements = array(); 316 317 // Do these first so we get interfaces ordered from most to least specific. 318 foreach ($symbol->getExtends() as $extends) { 319 if ($extends->getType() == 'interface') { 320 $implements[$extends->getName()] = array( 321 'interface' => $extends, 322 'via' => $symbol, 323 ); 324 } 325 } 326 327 // Now do parent interfaces. 328 foreach ($symbol->getExtends() as $extends) { 329 if ($extends->getType() == 'class') { 330 $implements += $this->getImplementsLineage($extends); 331 } 332 } 333 334 return $implements; 335 } 336 337 private function buildDefined( 338 PHUIPropertyListView $view, 339 DivinerLiveSymbol $symbol) { 340 341 $atom = $symbol->getAtom(); 342 $defined = $atom->getFile().':'.$atom->getLine(); 343 344 $link = $symbol->getBook()->getConfig('uri.source'); 345 if ($link) { 346 $link = strtr( 347 $link, 348 array( 349 '%%' => '%', 350 '%f' => phutil_escape_uri($atom->getFile()), 351 '%l' => phutil_escape_uri($atom->getLine()), 352 )); 353 $defined = phutil_tag( 354 'a', 355 array( 356 'href' => $link, 357 'target' => '_blank', 358 ), 359 $defined); 360 } 361 362 $view->addProperty(pht('Defined'), $defined); 363 } 364 365 private function composeMethods(DivinerLiveSymbol $symbol) { 366 $methods = $this->findMethods($symbol); 367 if (!$methods) { 368 return $methods; 369 } 370 371 foreach ($methods as $name => $method) { 372 // Check for "@task" on each parent, to find the most recently declared 373 // "@task". 374 $task = null; 375 foreach ($method['atoms'] as $key => $method_symbol) { 376 $atom = $method_symbol->getAtom(); 377 if ($atom->getDocblockMetaValue('task')) { 378 $task = $atom->getDocblockMetaValue('task'); 379 } 380 } 381 $methods[$name]['task'] = $task; 382 383 // Set 'inherited' if this atom has no implementation of the method. 384 if (last($method['implementations']) !== $symbol) { 385 $methods[$name]['inherited'] = true; 386 } else { 387 $methods[$name]['inherited'] = false; 388 } 389 } 390 391 return $methods; 392 } 393 394 private function findMethods(DivinerLiveSymbol $symbol) { 395 $child_specs = array(); 396 foreach ($symbol->getExtends() as $extends) { 397 if ($extends->getType() == DivinerAtom::TYPE_CLASS) { 398 $child_specs = $this->findMethods($extends); 399 } 400 } 401 402 foreach ($symbol->getChildren() as $child) { 403 if ($child->getType() == DivinerAtom::TYPE_METHOD) { 404 $name = $child->getName(); 405 if (isset($child_specs[$name])) { 406 $child_specs[$name]['atoms'][] = $child; 407 $child_specs[$name]['implementations'][] = $symbol; 408 } else { 409 $child_specs[$name] = array( 410 'atoms' => array($child), 411 'defined' => $symbol, 412 'implementations' => array($symbol), 413 ); 414 } 415 } 416 } 417 418 return $child_specs; 419 } 420 421 private function composeTasks(DivinerLiveSymbol $symbol) { 422 $extends_task_specs = array(); 423 foreach ($symbol->getExtends() as $extends) { 424 $extends_task_specs += $this->composeTasks($extends); 425 } 426 427 $task_specs = array(); 428 429 $tasks = $symbol->getAtom()->getDocblockMetaValue('task'); 430 if (strlen($tasks)) { 431 $tasks = phutil_split_lines($tasks, $retain_endings = false); 432 433 foreach ($tasks as $task) { 434 list($name, $title) = explode(' ', $task, 2); 435 $name = trim($name); 436 $title = trim($title); 437 438 $task_specs[$name] = array( 439 'name' => $name, 440 'title' => $title, 441 'defined' => $symbol, 442 ); 443 } 444 } 445 446 $specs = $task_specs + $extends_task_specs; 447 448 // Reorder "@tasks" in original declaration order. Basically, we want to 449 // use the documentation of the closest subclass, but put tasks which 450 // were declared by parents first. 451 $keys = array_keys($extends_task_specs); 452 $specs = array_select_keys($specs, $keys) + $specs; 453 454 return $specs; 455 } 456 457 private function renderFullSignature( 458 DivinerLiveSymbol $symbol, 459 $is_link = false) { 460 switch ($symbol->getType()) { 461 case DivinerAtom::TYPE_CLASS: 462 case DivinerAtom::TYPE_INTERFACE: 463 case DivinerAtom::TYPE_METHOD: 464 case DivinerAtom::TYPE_FUNCTION: 465 break; 466 default: 467 return $symbol->getTitle(); 468 } 469 470 $atom = $symbol->getAtom(); 471 472 $out = array(); 473 if ($atom->getProperty('final')) { 474 $out[] = 'final'; 475 } 476 477 if ($atom->getProperty('abstract')) { 478 $out[] = 'abstract'; 479 } 480 481 if ($atom->getProperty('access')) { 482 $out[] = $atom->getProperty('access'); 483 } 484 485 if ($atom->getProperty('static')) { 486 $out[] = 'static'; 487 } 488 489 switch ($symbol->getType()) { 490 case DivinerAtom::TYPE_CLASS: 491 case DivinerAtom::TYPE_INTERFACE: 492 $out[] = $symbol->getType(); 493 break; 494 case DivinerAtom::TYPE_FUNCTION: 495 switch ($atom->getLanguage()) { 496 case 'php': 497 $out[] = $symbol->getType(); 498 break; 499 } 500 break; 501 case DivinerAtom::TYPE_METHOD: 502 switch ($atom->getLanguage()) { 503 case 'php': 504 $out[] = DivinerAtom::TYPE_FUNCTION; 505 break; 506 } 507 break; 508 } 509 510 $anchor = null; 511 switch ($symbol->getType()) { 512 case DivinerAtom::TYPE_METHOD: 513 $anchor = $symbol->getType().'/'.$symbol->getName(); 514 break; 515 default: 516 break; 517 } 518 519 $out[] = phutil_tag( 520 $anchor ? 'a' : 'span', 521 array( 522 'class' => 'diviner-atom-signature-name', 523 'href' => $anchor ? '#'.$anchor : null, 524 'name' => $is_link ? null : $anchor, 525 ), 526 $symbol->getName()); 527 528 $out = phutil_implode_html(' ', $out); 529 530 $parameters = $atom->getProperty('parameters'); 531 if ($parameters !== null) { 532 $pout = array(); 533 foreach ($parameters as $parameter) { 534 $pout[] = idx($parameter, 'name', '...'); 535 } 536 $out = array($out, '('.implode(', ', $pout).')'); 537 } 538 539 return phutil_tag( 540 'span', 541 array( 542 'class' => 'diviner-atom-signature', 543 ), 544 $out); 545 } 546 547 private function buildParametersAndReturn(array $symbols) { 548 assert_instances_of($symbols, 'DivinerLiveSymbol'); 549 550 $symbols = array_reverse($symbols); 551 $out = array(); 552 553 $collected_parameters = null; 554 foreach ($symbols as $symbol) { 555 $parameters = $symbol->getAtom()->getProperty('parameters'); 556 if ($parameters !== null) { 557 if ($collected_parameters === null) { 558 $collected_parameters = array(); 559 } 560 foreach ($parameters as $key => $parameter) { 561 if (isset($collected_parameters[$key])) { 562 $collected_parameters[$key] += $parameter; 563 } else { 564 $collected_parameters[$key] = $parameter; 565 } 566 } 567 } 568 } 569 570 if (nonempty($parameters)) { 571 $out[] = id(new DivinerParameterTableView()) 572 ->setHeader(pht('Parameters')) 573 ->setParameters($parameters); 574 } 575 576 $collected_return = null; 577 foreach ($symbols as $symbol) { 578 $return = $symbol->getAtom()->getProperty('return'); 579 if ($return) { 580 if ($collected_return) { 581 $collected_return += $return; 582 } else { 583 $collected_return = $return; 584 } 585 } 586 } 587 588 if (nonempty($return)) { 589 $out[] = id(new DivinerReturnTableView()) 590 ->setHeader(pht('Return')) 591 ->setReturn($collected_return); 592 } 593 594 return $out; 595 } 596 597 private function renderDocumentationText( 598 DivinerLiveSymbol $symbol, 599 PhabricatorMarkupEngine $engine) { 600 601 $field = 'default'; 602 $content = $engine->getOutput($symbol, $field); 603 604 if (strlen(trim($symbol->getMarkupText($field)))) { 605 $content = phutil_tag( 606 'div', 607 array( 608 'class' => 'phabricator-remarkup', 609 ), 610 $content); 611 } else { 612 $atom = $symbol->getAtom(); 613 $content = phutil_tag( 614 'div', 615 array( 616 'class' => 'diviner-message-not-documented', 617 ), 618 DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType())); 619 } 620 621 return $content; 622 } 623 624 private function renderMethodDocumentationText( 625 DivinerLiveSymbol $parent, 626 array $spec, 627 PhabricatorMarkupEngine $engine) { 628 629 $symbols = array_values($spec['atoms']); 630 $implementations = array_values($spec['implementations']); 631 632 $field = 'default'; 633 634 $out = array(); 635 foreach ($symbols as $key => $symbol) { 636 $impl = $implementations[$key]; 637 if ($impl !== $parent) { 638 if (!strlen(trim($symbol->getMarkupText($field)))) { 639 continue; 640 } 641 } 642 643 $doc = $this->renderDocumentationText($symbol, $engine); 644 645 if (($impl !== $parent) || $out) { 646 $where = id(new PHUIBoxView()) 647 ->addPadding(PHUI::PADDING_MEDIUM_LEFT) 648 ->addPadding(PHUI::PADDING_MEDIUM_RIGHT) 649 ->addClass('diviner-method-implementation-header') 650 ->appendChild($impl->getName()); 651 $doc = array($where, $doc); 652 653 if ($impl !== $parent) { 654 $doc = phutil_tag( 655 'div', 656 array( 657 'class' => 'diviner-method-implementation-inherited', 658 ), 659 $doc); 660 } 661 } 662 663 $out[] = $doc; 664 } 665 666 // If we only have inherited implementations but none have documentation, 667 // render the last one here so we get the "this thing has no documentation" 668 // element. 669 if (!$out) { 670 $out[] = $this->renderDocumentationText($symbol, $engine); 671 } 672 673 return $out; 674 } 675 676 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |