Path: blob/master/src/applications/diviner/controller/DivinerAtomController.php
12256 views
<?php12final class DivinerAtomController extends DivinerController {34public function shouldAllowPublic() {5return true;6}78public function handleRequest(AphrontRequest $request) {9$viewer = $request->getUser();1011$book_name = $request->getURIData('book');12$atom_type = $request->getURIData('type');13$atom_name = $request->getURIData('name');14$atom_context = nonempty($request->getURIData('context'), null);15$atom_index = nonempty($request->getURIData('index'), null);1617require_celerity_resource('diviner-shared-css');1819$book = id(new DivinerBookQuery())20->setViewer($viewer)21->withNames(array($book_name))22->executeOne();2324if (!$book) {25return new Aphront404Response();26}2728$symbol = id(new DivinerAtomQuery())29->setViewer($viewer)30->withBookPHIDs(array($book->getPHID()))31->withTypes(array($atom_type))32->withNames(array($atom_name))33->withContexts(array($atom_context))34->withIndexes(array($atom_index))35->withIsDocumentable(true)36->needAtoms(true)37->needExtends(true)38->needChildren(true)39->executeOne();4041if (!$symbol) {42return new Aphront404Response();43}4445$atom = $symbol->getAtom();46$crumbs = $this->buildApplicationCrumbs();47$crumbs->setBorder(true);4849$crumbs->addTextCrumb(50$book->getShortTitle(),51'/book/'.$book->getName().'/');5253$atom_short_title = $atom54? $atom->getDocblockMetaValue('short', $symbol->getTitle())55: $symbol->getTitle();5657$crumbs->addTextCrumb($atom_short_title);5859$header = id(new PHUIHeaderView())60->setHeader($this->renderFullSignature($symbol));6162$properties = new PHUIPropertyListView();6364$group = $atom ? $atom->getProperty('group') : $symbol->getGroupName();65if ($group) {66$group_name = $book->getGroupName($group);67} else {68$group_name = null;69}7071$prop_list = new PHUIPropertyGroupView();72$prop_list->addPropertyList($properties);7374$document = id(new PHUIDocumentView())75->setBook($book->getTitle(), $group_name)76->setHeader($header)77->addClass('diviner-view');7879if ($atom) {80$this->buildDefined($properties, $symbol);81$this->buildExtendsAndImplements($properties, $symbol);82$this->buildRepository($properties, $symbol);8384$warnings = $atom->getWarnings();85if ($warnings) {86$warnings = id(new PHUIInfoView())87->setErrors($warnings)88->setTitle(pht('Documentation Warnings'))89->setSeverity(PHUIInfoView::SEVERITY_WARNING);90}9192$document->appendChild($warnings);93}9495$methods = $this->composeMethods($symbol);9697$field = 'default';98$engine = id(new PhabricatorMarkupEngine())99->setViewer($viewer)100->addObject($symbol, $field);101foreach ($methods as $method) {102foreach ($method['atoms'] as $matom) {103$engine->addObject($matom, $field);104}105}106$engine->process();107108if ($atom) {109$content = $this->renderDocumentationText($symbol, $engine);110$document->appendChild($content);111}112113$toc = $engine->getEngineMetadata(114$symbol,115$field,116PhutilRemarkupHeaderBlockRule::KEY_HEADER_TOC,117array());118119if (!$atom) {120$document->appendChild(121id(new PHUIInfoView())122->setSeverity(PHUIInfoView::SEVERITY_NOTICE)123->appendChild(pht('This atom no longer exists.')));124}125126if ($atom) {127$document->appendChild($this->buildParametersAndReturn(array($symbol)));128}129130if ($methods) {131$tasks = $this->composeTasks($symbol);132133if ($tasks) {134$methods_by_task = igroup($methods, 'task');135136// Add phantom tasks for methods which have a "@task" name that isn't137// documented anywhere, or methods that have no "@task" name.138foreach ($methods_by_task as $task => $ignored) {139if (empty($tasks[$task])) {140$tasks[$task] = array(141'name' => $task,142'title' => $task ? $task : pht('Other Methods'),143'defined' => $symbol,144);145}146}147148$section = id(new DivinerSectionView())149->setHeader(pht('Tasks'));150151foreach ($tasks as $spec) {152$section->addContent(153id(new PHUIHeaderView())154->setNoBackground(true)155->setHeader($spec['title']));156157$task_methods = idx($methods_by_task, $spec['name'], array());158159$box_content = array();160if ($task_methods) {161$list_items = array();162foreach ($task_methods as $task_method) {163$atom = last($task_method['atoms']);164165$item = $this->renderFullSignature($atom, true);166167if (strlen($atom->getSummary())) {168$item = array(169$item,170" \xE2\x80\x94 ",171$atom->getSummary(),172);173}174175$list_items[] = phutil_tag('li', array(), $item);176}177178$box_content[] = phutil_tag(179'ul',180array(181'class' => 'diviner-list',182),183$list_items);184} else {185$no_methods = pht('No methods for this task.');186$box_content = phutil_tag('em', array(), $no_methods);187}188189$inner_box = phutil_tag_div('diviner-task-items', $box_content);190$section->addContent($inner_box);191}192$document->appendChild($section);193}194195$section = id(new DivinerSectionView())196->setHeader(pht('Methods'));197198foreach ($methods as $spec) {199$matom = last($spec['atoms']);200$method_header = id(new PHUIHeaderView())201->setNoBackground(true);202203$inherited = $spec['inherited'];204if ($inherited) {205$method_header->addTag(206id(new PHUITagView())207->setType(PHUITagView::TYPE_STATE)208->setBackgroundColor(PHUITagView::COLOR_GREY)209->setName(pht('Inherited')));210}211212$method_header->setHeader($this->renderFullSignature($matom));213214$section->addContent(215array(216$method_header,217$this->renderMethodDocumentationText($symbol, $spec, $engine),218$this->buildParametersAndReturn($spec['atoms']),219));220}221$document->appendChild($section);222}223224if ($toc) {225$side = new PHUIListView();226$side->addMenuItem(227id(new PHUIListItemView())228->setName(pht('Contents'))229->setType(PHUIListItemView::TYPE_LABEL));230foreach ($toc as $key => $entry) {231$side->addMenuItem(232id(new PHUIListItemView())233->setName($entry[1])234->setHref('#'.$key));235}236237$document->setToc($side);238}239240$prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list);241242return $this->newPage()243->setTitle($symbol->getTitle())244->setCrumbs($crumbs)245->appendChild(array(246$document,247$prop_list,248));249}250251private function buildExtendsAndImplements(252PHUIPropertyListView $view,253DivinerLiveSymbol $symbol) {254255$lineage = $this->getExtendsLineage($symbol);256if ($lineage) {257$tags = array();258foreach ($lineage as $item) {259$tags[] = $this->renderAtomTag($item);260}261262$caret = phutil_tag('span', array('class' => 'caret-right msl msr'));263$tags = phutil_implode_html($caret, $tags);264$view->addProperty(pht('Extends'), $tags);265}266267$implements = $this->getImplementsLineage($symbol);268if ($implements) {269$items = array();270foreach ($implements as $spec) {271$via = $spec['via'];272$iface = $spec['interface'];273if ($via == $symbol) {274$items[] = $this->renderAtomTag($iface);275} else {276$items[] = array(277$this->renderAtomTag($iface),278" \xE2\x97\x80 ",279$this->renderAtomTag($via),280);281}282}283284$view->addProperty(285pht('Implements'),286phutil_implode_html(phutil_tag('br'), $items));287}288}289290private function buildRepository(291PHUIPropertyListView $view,292DivinerLiveSymbol $symbol) {293294if (!$symbol->getRepositoryPHID()) {295return;296}297298$view->addProperty(299pht('Repository'),300$this->getViewer()->renderHandle($symbol->getRepositoryPHID()));301}302303private function renderAtomTag(DivinerLiveSymbol $symbol) {304return id(new PHUITagView())305->setType(PHUITagView::TYPE_OBJECT)306->setName($symbol->getName())307->setHref($symbol->getURI());308}309310private function getExtendsLineage(DivinerLiveSymbol $symbol) {311foreach ($symbol->getExtends() as $extends) {312if ($extends->getType() == 'class') {313$lineage = $this->getExtendsLineage($extends);314$lineage[] = $extends;315return $lineage;316}317}318return array();319}320321private function getImplementsLineage(DivinerLiveSymbol $symbol) {322$implements = array();323324// Do these first so we get interfaces ordered from most to least specific.325foreach ($symbol->getExtends() as $extends) {326if ($extends->getType() == 'interface') {327$implements[$extends->getName()] = array(328'interface' => $extends,329'via' => $symbol,330);331}332}333334// Now do parent interfaces.335foreach ($symbol->getExtends() as $extends) {336if ($extends->getType() == 'class') {337$implements += $this->getImplementsLineage($extends);338}339}340341return $implements;342}343344private function buildDefined(345PHUIPropertyListView $view,346DivinerLiveSymbol $symbol) {347348$atom = $symbol->getAtom();349$defined = $atom->getFile().':'.$atom->getLine();350351$link = $symbol->getBook()->getConfig('uri.source');352if ($link) {353$link = strtr(354$link,355array(356'%%' => '%',357'%f' => phutil_escape_uri($atom->getFile()),358'%l' => phutil_escape_uri($atom->getLine()),359));360$defined = phutil_tag(361'a',362array(363'href' => $link,364'target' => '_blank',365),366$defined);367}368369$view->addProperty(pht('Defined'), $defined);370}371372private function composeMethods(DivinerLiveSymbol $symbol) {373$methods = $this->findMethods($symbol);374if (!$methods) {375return $methods;376}377378foreach ($methods as $name => $method) {379// Check for "@task" on each parent, to find the most recently declared380// "@task".381$task = null;382foreach ($method['atoms'] as $key => $method_symbol) {383$atom = $method_symbol->getAtom();384if ($atom->getDocblockMetaValue('task')) {385$task = $atom->getDocblockMetaValue('task');386}387}388$methods[$name]['task'] = $task;389390// Set 'inherited' if this atom has no implementation of the method.391if (last($method['implementations']) !== $symbol) {392$methods[$name]['inherited'] = true;393} else {394$methods[$name]['inherited'] = false;395}396}397398return $methods;399}400401private function findMethods(DivinerLiveSymbol $symbol) {402$child_specs = array();403foreach ($symbol->getExtends() as $extends) {404if ($extends->getType() == DivinerAtom::TYPE_CLASS) {405$child_specs = $this->findMethods($extends);406}407}408409foreach ($symbol->getChildren() as $child) {410if ($child->getType() == DivinerAtom::TYPE_METHOD) {411$name = $child->getName();412if (isset($child_specs[$name])) {413$child_specs[$name]['atoms'][] = $child;414$child_specs[$name]['implementations'][] = $symbol;415} else {416$child_specs[$name] = array(417'atoms' => array($child),418'defined' => $symbol,419'implementations' => array($symbol),420);421}422}423}424425return $child_specs;426}427428private function composeTasks(DivinerLiveSymbol $symbol) {429$extends_task_specs = array();430foreach ($symbol->getExtends() as $extends) {431$extends_task_specs += $this->composeTasks($extends);432}433434$task_specs = array();435436$tasks = $symbol->getAtom()->getDocblockMetaValue('task');437438if (!is_array($tasks)) {439if (strlen($tasks)) {440$tasks = array($tasks);441} else {442$tasks = array();443}444}445446if ($tasks) {447foreach ($tasks as $task) {448list($name, $title) = explode(' ', $task, 2);449$name = trim($name);450$title = trim($title);451452$task_specs[$name] = array(453'name' => $name,454'title' => $title,455'defined' => $symbol,456);457}458}459460$specs = $task_specs + $extends_task_specs;461462// Reorder "@tasks" in original declaration order. Basically, we want to463// use the documentation of the closest subclass, but put tasks which464// were declared by parents first.465$keys = array_keys($extends_task_specs);466$specs = array_select_keys($specs, $keys) + $specs;467468return $specs;469}470471private function renderFullSignature(472DivinerLiveSymbol $symbol,473$is_link = false) {474475switch ($symbol->getType()) {476case DivinerAtom::TYPE_CLASS:477case DivinerAtom::TYPE_INTERFACE:478case DivinerAtom::TYPE_METHOD:479case DivinerAtom::TYPE_FUNCTION:480break;481default:482return $symbol->getTitle();483}484485$atom = $symbol->getAtom();486487$out = array();488489if ($atom) {490if ($atom->getProperty('final')) {491$out[] = 'final';492}493494if ($atom->getProperty('abstract')) {495$out[] = 'abstract';496}497498if ($atom->getProperty('access')) {499$out[] = $atom->getProperty('access');500}501502if ($atom->getProperty('static')) {503$out[] = 'static';504}505}506507switch ($symbol->getType()) {508case DivinerAtom::TYPE_CLASS:509case DivinerAtom::TYPE_INTERFACE:510$out[] = $symbol->getType();511break;512case DivinerAtom::TYPE_FUNCTION:513switch ($atom->getLanguage()) {514case 'php':515$out[] = $symbol->getType();516break;517}518break;519case DivinerAtom::TYPE_METHOD:520switch ($atom->getLanguage()) {521case 'php':522$out[] = DivinerAtom::TYPE_FUNCTION;523break;524}525break;526}527528$anchor = null;529switch ($symbol->getType()) {530case DivinerAtom::TYPE_METHOD:531$anchor = $symbol->getType().'/'.$symbol->getName();532break;533default:534break;535}536537$out[] = phutil_tag(538$anchor ? 'a' : 'span',539array(540'class' => 'diviner-atom-signature-name',541'href' => $anchor ? '#'.$anchor : null,542'name' => $is_link ? null : $anchor,543),544$symbol->getName());545546$out = phutil_implode_html(' ', $out);547548if ($atom) {549$parameters = $atom->getProperty('parameters');550if ($parameters !== null) {551$pout = array();552foreach ($parameters as $parameter) {553$pout[] = idx($parameter, 'name', '...');554}555$out = array($out, '('.implode(', ', $pout).')');556}557}558559return phutil_tag(560'span',561array(562'class' => 'diviner-atom-signature',563),564$out);565}566567private function buildParametersAndReturn(array $symbols) {568assert_instances_of($symbols, 'DivinerLiveSymbol');569570$symbols = array_reverse($symbols);571$out = array();572573$collected_parameters = null;574foreach ($symbols as $symbol) {575$parameters = $symbol->getAtom()->getProperty('parameters');576if ($parameters !== null) {577if ($collected_parameters === null) {578$collected_parameters = array();579}580foreach ($parameters as $key => $parameter) {581if (isset($collected_parameters[$key])) {582$collected_parameters[$key] += $parameter;583} else {584$collected_parameters[$key] = $parameter;585}586}587}588}589590if (nonempty($parameters)) {591$out[] = id(new DivinerParameterTableView())592->setHeader(pht('Parameters'))593->setParameters($parameters);594}595596$collected_return = null;597foreach ($symbols as $symbol) {598$return = $symbol->getAtom()->getProperty('return');599if ($return) {600if ($collected_return) {601$collected_return += $return;602} else {603$collected_return = $return;604}605}606}607608if (nonempty($return)) {609$out[] = id(new DivinerReturnTableView())610->setHeader(pht('Return'))611->setReturn($collected_return);612}613614return $out;615}616617private function renderDocumentationText(618DivinerLiveSymbol $symbol,619PhabricatorMarkupEngine $engine) {620621$field = 'default';622$content = $engine->getOutput($symbol, $field);623624if (strlen(trim($symbol->getMarkupText($field)))) {625$content = phutil_tag(626'div',627array(628'class' => 'phabricator-remarkup diviner-remarkup-section',629),630$content);631} else {632$atom = $symbol->getAtom();633$content = phutil_tag(634'div',635array(636'class' => 'diviner-message-not-documented',637),638DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()));639}640641return $content;642}643644private function renderMethodDocumentationText(645DivinerLiveSymbol $parent,646array $spec,647PhabricatorMarkupEngine $engine) {648649$symbols = array_values($spec['atoms']);650$implementations = array_values($spec['implementations']);651652$field = 'default';653654$out = array();655foreach ($symbols as $key => $symbol) {656$impl = $implementations[$key];657if ($impl !== $parent) {658if (!strlen(trim($symbol->getMarkupText($field)))) {659continue;660}661}662663$doc = $this->renderDocumentationText($symbol, $engine);664665if (($impl !== $parent) || $out) {666$where = id(new PHUIBoxView())667->addClass('diviner-method-implementation-header')668->appendChild($impl->getName());669$doc = array($where, $doc);670671if ($impl !== $parent) {672$doc = phutil_tag(673'div',674array(675'class' => 'diviner-method-implementation-inherited',676),677$doc);678}679}680681$out[] = $doc;682}683684// If we only have inherited implementations but none have documentation,685// render the last one here so we get the "this thing has no documentation"686// element.687if (!$out) {688$out[] = $this->renderDocumentationText($symbol, $engine);689}690691return $out;692}693694}695696697