[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/diviner/controller/ -> DivinerAtomController.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1