diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php index b224dde4d7..b130dbf2a4 100644 --- a/src/applications/diviner/atom/DivinerAtom.php +++ b/src/applications/diviner/atom/DivinerAtom.php @@ -1,426 +1,437 @@ getBook(), $this->getType(), $this->getContext(), $this->getName(), $this->getFile(), sprintf('%08', $this->getLine()), )); } public function setBook($book) { $this->book = $book; return $this; } public function getBook() { return $this->book; } public function setContext($context) { $this->context = $context; return $this; } public function getContext() { return $this->context; } public static function getAtomSerializationVersion() { return 2; } public function addWarning($warning) { $this->warnings[] = $warning; return $this; } public function getWarnings() { return $this->warnings; } public function setDocblockRaw($docblock_raw) { $this->docblockRaw = $docblock_raw; $parser = new PhutilDocblockParser(); list($text, $meta) = $parser->parse($docblock_raw); $this->docblockText = $text; $this->docblockMeta = $meta; return $this; } public function getDocblockRaw() { return $this->docblockRaw; } public function getDocblockText() { if ($this->docblockText === null) { throw new Exception("Call setDocblockRaw() before getDocblockText()!"); } return $this->docblockText; } public function getDocblockMeta() { if ($this->docblockMeta === null) { throw new Exception("Call setDocblockRaw() before getDocblockMeta()!"); } return $this->docblockMeta; } public function getDocblockMetaValue($key, $default = null) { $meta = $this->getDocblockMeta(); return idx($meta, $key, $default); } public function setDocblockMetaValue($key, $value) { $meta = $this->getDocblockMeta(); $meta[$key] = $value; $this->docblockMeta = $meta; return $this; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setFile($file) { $this->file = $file; return $this; } public function getFile() { return $this->file; } public function setLine($line) { $this->line = $line; return $this; } public function getLine() { return $this->line; } public function setContentRaw($content_raw) { $this->contentRaw = $content_raw; return $this; } public function getContentRaw() { return $this->contentRaw; } public function setHash($hash) { $this->hash = $hash; return $this; } public function addLink(DivinerAtomRef $ref) { $this->links[] = $ref; return $this; } public function addExtends(DivinerAtomRef $ref) { $this->extends[] = $ref; return $this; } public function getLinkDictionaries() { return mpull($this->links, 'toDictionary'); } public function getExtendsDictionaries() { return mpull($this->extends, 'toDictionary'); } public function getExtends() { return $this->extends; } public function getHash() { if ($this->hash) { return $this->hash; } $parts = array( $this->getBook(), $this->getType(), $this->getName(), $this->getFile(), $this->getLine(), $this->getLength(), $this->getLanguage(), $this->getContentRaw(), $this->getDocblockRaw(), $this->getProperties(), $this->getChildHashes(), mpull($this->extends, 'toHash'), mpull($this->links, 'toHash'), ); $this->hash = md5(serialize($parts)).'N'; return $this->hash; } public function setLength($length) { $this->length = $length; return $this; } public function getLength() { return $this->length; } public function setLanguage($language) { $this->language = $language; return $this; } public function getLanguage() { return $this->language; } public function addChildHash($child_hash) { $this->childHashes[] = $child_hash; return $this; } public function getChildHashes() { if (!$this->childHashes && $this->children) { $this->childHashes = mpull($this->children, 'getHash'); } return $this->childHashes; } public function setParentHash($parent_hash) { if ($this->parentHash) { throw new Exception("Atom already has a parent!"); } $this->parentHash = $parent_hash; return $this; } public function hasParent() { return $this->parent || $this->parentHash; } public function setParent(DivinerAtom $atom) { if ($this->parentHash) { throw new Exception("Parent hash has already been computed!"); } $this->parent = $atom; return $this; } public function getParentHash() { if ($this->parent && !$this->parentHash) { $this->parentHash = $this->parent->getHash(); } return $this->parentHash; } public function addChild(DivinerAtom $atom) { if ($this->childHashes) { throw new Exception("Child hashes have already been computed!"); } $atom->setParent($this); $this->children[] = $atom; return $this; } public function getURI() { $parts = array(); $parts[] = phutil_escape_uri_path_component($this->getType()); if ($this->getContext()) { $parts[] = phutil_escape_uri_path_component($this->getContext()); } $parts[] = phutil_escape_uri_path_component($this->getName()); $parts[] = null; return implode('/', $parts); } public function toDictionary() { // NOTE: If you change this format, bump the format version in // getAtomSerializationVersion(). return array( 'book' => $this->getBook(), 'type' => $this->getType(), 'name' => $this->getName(), 'file' => $this->getFile(), 'line' => $this->getLine(), 'hash' => $this->getHash(), 'uri' => $this->getURI(), 'length' => $this->getLength(), 'context' => $this->getContext(), 'language' => $this->getLanguage(), 'docblockRaw' => $this->getDocblockRaw(), 'warnings' => $this->getWarnings(), 'parentHash' => $this->getParentHash(), 'childHashes' => $this->getChildHashes(), 'extends' => $this->getExtendsDictionaries(), 'links' => $this->getLinkDictionaries(), 'ref' => $this->getRef()->toDictionary(), 'properties' => $this->getProperties(), ); } public function getRef() { $title = null; if ($this->docblockMeta) { $title = $this->getDocblockMetaValue('title'); } return id(new DivinerAtomRef()) ->setBook($this->getBook()) ->setContext($this->getContext()) ->setType($this->getType()) ->setName($this->getName()) ->setTitle($title) ->setGroup($this->getProperty('group')); } public static function newFromDictionary(array $dictionary) { $atom = id(new DivinerAtom()) ->setBook(idx($dictionary, 'book')) ->setType(idx($dictionary, 'type')) ->setName(idx($dictionary, 'name')) ->setFile(idx($dictionary, 'file')) ->setLine(idx($dictionary, 'line')) ->setHash(idx($dictionary, 'hash')) ->setLength(idx($dictionary, 'length')) ->setContext(idx($dictionary, 'context')) ->setLanguage(idx($dictionary, 'language')) ->setParentHash(idx($dictionary, 'parentHash')) ->setDocblockRaw(idx($dictionary, 'docblockRaw')) ->setProperties(idx($dictionary, 'properties')); foreach (idx($dictionary, 'warnings', array()) as $warning) { $atom->addWarning($warning); } foreach (idx($dictionary, 'childHashes', array()) as $child) { $atom->addChildHash($child); } foreach (idx($dictionary, 'extends', array()) as $extends) { $atom->addExtends(DivinerAtomRef::newFromDictionary($extends)); } return $atom; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; } public function getProperties() { return $this->properties; } public function setProperties(array $properties) { $this->properties = $properties; return $this; } public static function getThisAtomIsNotDocumentedString($type) { switch ($type) { case self::TYPE_FILE: return pht('This file is not documented.'); case self::TYPE_FUNCTION: return pht('This function is not documented.'); case self::TYPE_CLASS: return pht('This class is not documented.'); case self::TYPE_ARTICLE: return pht('This article is not documented.'); case self::TYPE_METHOD: return pht('This method is not documented.'); case self::TYPE_INTERFACE: return pht('This interface is not documented.'); default: phlog("Need translation for '{$type}'."); return pht('This %s is not documented.', $type); } } + public static function getAllTypes() { + return array( + self::TYPE_FILE, + self::TYPE_FUNCTION, + self::TYPE_CLASS, + self::TYPE_ARTICLE, + self::TYPE_METHOD, + self::TYPE_INTERFACE, + ); + } + public static function getAtomTypeNameString($type) { switch ($type) { case self::TYPE_FILE: return pht('File'); case self::TYPE_FUNCTION: return pht('Function'); case self::TYPE_CLASS: return pht('Class'); case self::TYPE_ARTICLE: return pht('Article'); case self::TYPE_METHOD: return pht('Method'); case self::TYPE_INTERFACE: return pht('Interface'); default: phlog("Need translation for '{$type}'."); return ucwords($type); } } } diff --git a/src/applications/diviner/atom/DivinerAtomRef.php b/src/applications/diviner/atom/DivinerAtomRef.php index b536ed6223..87e2ce940a 100644 --- a/src/applications/diviner/atom/DivinerAtomRef.php +++ b/src/applications/diviner/atom/DivinerAtomRef.php @@ -1,206 +1,210 @@ getName(), $this->getType(), $this->getContext(), $this->getBook(), $this->getIndex(), )); } public function setIndex($index) { $this->index = $index; return $this; } public function getIndex() { return $this->index; } public function setSummary($summary) { $this->summary = $summary; return $this; } public function getSummary() { return $this->summary; } public function setName($name) { $normal_name = self::normalizeString($name); if (preg_match('/^@[0-9]+$/', $normal_name)) { throw new Exception( "Atom names must not be in the form '/@\d+/'. This pattern is ". "reserved for disambiguating atoms with similar names."); } $this->name = $normal_name; return $this; } public function getName() { return $this->name; } public function setType($type) { $this->type = self::normalizeString($type); return $this; } public function getType() { return $this->type; } public function setContext($context) { if ($context === null) { $this->context = $context; } else { $this->context = self::normalizeString($context); } return $this; } public function getContext() { return $this->context; } public function setBook($book) { if ($book === null) { $this->book = $book; } else { $this->book = self::normalizeString($book); } return $this; } public function getBook() { return $this->book; } public function setGroup($group) { $this->group = $group; return $this; } public function getGroup() { return $this->group; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } public function getTitleSlug() { return self::normalizeTitleString($this->getTitle()); } public function toDictionary() { return array( 'book' => $this->getBook(), 'context' => $this->getContext(), 'type' => $this->getType(), 'name' => $this->getName(), 'group' => $this->getGroup(), 'index' => $this->getIndex(), 'summary' => $this->getSummary(), 'title' => $this->getTitle(), ); } public function toHash() { $dict = $this->toDictionary(); unset($dict['group']); unset($dict['index']); unset($dict['summary']); unset($dict['title']); ksort($dict); return md5(serialize($dict)).'S'; } public static function newFromDictionary(array $dict) { $obj = new DivinerAtomRef(); $obj->setBook(idx($dict, 'book')); $obj->setContext(idx($dict, 'context')); $obj->setType(idx($dict, 'type')); $obj->setName(idx($dict, 'name')); $obj->group = idx($dict, 'group'); $obj->index = idx($dict, 'index'); $obj->summary = idx($dict, 'summary'); $obj->title = idx($dict, 'title'); return $obj; } public static function normalizeString($str) { // These characters create problems on the filesystem or in URIs. Replace // them with non-problematic appoximations (instead of simply removing them) // to keep the URIs fairly useful and avoid unnecessary collisions. These // approximations are selected based on some domain knowledge of common // languages: where a character is used as a delimiter, it is more helpful // to replace it with a "." or a ":" or similar, while it's better if // operator overloads read as, e.g., "operator_div". $map = array( // Hopefully not used anywhere by anything. '#' => '.', // Used in Ruby methods. '?' => 'Q', // Used in PHP namespaces. '\\' => '.', // Used in "operator +" in C++. '+' => 'plus', // Used in "operator %" in C++. '%' => 'mod', // Used in "operator /" in C++. '/' => 'div', ); $str = str_replace(array_keys($map), array_values($map), $str); // Replace all spaces with underscores. $str = preg_replace('/ +/', '_', $str); // Replace control characters with "X". $str = preg_replace('/[\x00-\x19]/', 'X', $str); // Replace specific problematic names with alternative names. $alternates = array( '.' => 'dot', '..' => 'dotdot', '' => 'null', ); return idx($alternates, $str, $str); } public static function normalizeTitleString($str) { + // Remove colons from titles. This is mostly to accommodate legacy rules + // from the old Diviner, which generated a significant number of article + // URIs without colons present in the titles. + $str = str_replace(':', '', $str); $str = self::normalizeString($str); return phutil_utf8_strtolower($str); } } diff --git a/src/applications/diviner/controller/DivinerAtomListController.php b/src/applications/diviner/controller/DivinerAtomListController.php index 0da8842e56..1bddea8658 100644 --- a/src/applications/diviner/controller/DivinerAtomListController.php +++ b/src/applications/diviner/controller/DivinerAtomListController.php @@ -1,32 +1,54 @@ key = idx($data, 'key', 'all'); } public function processRequest() { $request = $this->getRequest(); $controller = id(new PhabricatorApplicationSearchController($request)) ->setQueryKey($this->key) ->setSearchEngine(new DivinerAtomSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function renderResultsList( array $symbols, PhabricatorSavedQuery $query) { - return $this->renderAtomList($symbols); + + assert_instances_of($symbols, 'DivinerLiveSymbol'); + + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + foreach ($symbols as $symbol) { + $type = $symbol->getType(); + $type_name = DivinerAtom::getAtomTypeNameString($type); + + $item = id(new PHUIObjectItemView()) + ->setHeader($symbol->getTitle()) + ->setHref($symbol->getURI()) + ->addAttribute($symbol->getSummary()) + ->addIcon('none', $type_name); + + $list->addItem($item); + } + + return $list; } } diff --git a/src/applications/diviner/query/DivinerAtomQuery.php b/src/applications/diviner/query/DivinerAtomQuery.php index df6124293f..6dc3ff0763 100644 --- a/src/applications/diviner/query/DivinerAtomQuery.php +++ b/src/applications/diviner/query/DivinerAtomQuery.php @@ -1,432 +1,449 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withBookPHIDs(array $phids) { $this->bookPHIDs = $phids; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withContexts(array $contexts) { $this->contexts = $contexts; return $this; } public function withIndexes(array $indexes) { $this->indexes = $indexes; return $this; } public function withNodeHashes(array $hashes) { $this->nodeHashes = $hashes; return $this; } public function withTitles($titles) { $this->titles = $titles; return $this; } + public function withNameContains($text) { + $this->nameContains = $text; + return $this; + } + public function needAtoms($need) { $this->needAtoms = $need; return $this; } public function needChildren($need) { $this->needChildren = $need; return $this; } /** * Include "ghosts", which are symbols which used to exist but do not exist * currently (for example, a function which existed in an older version of * the codebase but was deleted). * * These symbols had PHIDs assigned to them, and may have other sorts of * metadata that we don't want to lose (like comments or flags), so we don't * delete them outright. They might also come back in the future: the change * which deleted the symbol might be reverted, or the documentation might * have been generated incorrectly by accident. In these cases, we can * restore the original data. * * However, most callers are not interested in these symbols, so they are * excluded by default. You can use this method to include them in results. * * @param bool True to include ghosts. * @return this */ public function withIncludeGhosts($include) { $this->includeGhosts = $include; return $this; } public function needExtends($need) { $this->needExtends = $need; return $this; } public function withIncludeUndocumentable($include) { $this->includeUndocumentable = $include; return $this; } protected function loadPage() { $table = new DivinerLiveSymbol(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $atoms) { $books = array_unique(mpull($atoms, 'getBookPHID')); $books = id(new DivinerBookQuery()) ->setViewer($this->getViewer()) ->withPHIDs($books) ->execute(); $books = mpull($books, null, 'getPHID'); foreach ($atoms as $key => $atom) { $book = idx($books, $atom->getBookPHID()); if (!$book) { unset($atoms[$key]); continue; } $atom->attachBook($book); } if ($this->needAtoms) { $atom_data = id(new DivinerLiveAtom())->loadAllWhere( 'symbolPHID IN (%Ls)', mpull($atoms, 'getPHID')); $atom_data = mpull($atom_data, null, 'getSymbolPHID'); foreach ($atoms as $key => $atom) { $data = idx($atom_data, $atom->getPHID()); if (!$data) { unset($atoms[$key]); continue; } $atom->attachAtom($data); } } // Load all of the symbols this symbol extends, recursively. Commonly, // this means all the ancestor classes and interfaces it extends and // implements. if ($this->needExtends) { // First, load all the matching symbols by name. This does 99% of the // work in most cases, assuming things are named at all reasonably. $names = array(); foreach ($atoms as $atom) { foreach ($atom->getAtom()->getExtends() as $xref) { $names[] = $xref->getName(); } } if ($names) { $xatoms = id(new DivinerAtomQuery()) ->setViewer($this->getViewer()) ->withNames($names) ->needExtends(true) ->needAtoms(true) ->needChildren($this->needChildren) ->execute(); $xatoms = mgroup($xatoms, 'getName', 'getType', 'getBookPHID'); } else { $xatoms = array(); } foreach ($atoms as $atom) { $alang = $atom->getAtom()->getLanguage(); $extends = array(); foreach ($atom->getAtom()->getExtends() as $xref) { // If there are no symbols of the matching name and type, we can't // resolve this. if (empty($xatoms[$xref->getName()][$xref->getType()])) { continue; } // If we found matches in the same documentation book, prefer them // over other matches. Otherwise, look at all the the matches. $matches = $xatoms[$xref->getName()][$xref->getType()]; if (isset($matches[$atom->getBookPHID()])) { $maybe = $matches[$atom->getBookPHID()]; } else { $maybe = array_mergev($matches); } if (!$maybe) { continue; } // Filter out matches in a different language, since, e.g., PHP // classes can not implement JS classes. $same_lang = array(); foreach ($maybe as $xatom) { if ($xatom->getAtom()->getLanguage() == $alang) { $same_lang[] = $xatom; } } if (!$same_lang) { continue; } // If we have duplicates remaining, just pick the first one. There's // nothing more we can do to figure out which is the real one. $extends[] = head($same_lang); } $atom->attachExtends($extends); } } if ($this->needChildren) { $child_hashes = $this->getAllChildHashes($atoms, $this->needExtends); if ($child_hashes) { $children = id(new DivinerAtomQuery()) ->setViewer($this->getViewer()) ->withIncludeUndocumentable(true) ->withNodeHashes($child_hashes) ->needAtoms($this->needAtoms) ->execute(); $children = mpull($children, null, 'getNodeHash'); } else { $children = array(); } $this->attachAllChildren($atoms, $children, $this->needExtends); } return $atoms; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->bookPHIDs) { $where[] = qsprintf( $conn_r, 'bookPHID IN (%Ls)', $this->bookPHIDs); } if ($this->types) { $where[] = qsprintf( $conn_r, 'type IN (%Ls)', $this->types); } if ($this->names) { $where[] = qsprintf( $conn_r, 'name IN (%Ls)', $this->names); } if ($this->titles) { $hashes = array(); foreach ($this->titles as $title) { $slug = DivinerAtomRef::normalizeTitleString($title); $hash = PhabricatorHash::digestForIndex($slug); $hashes[] = $hash; } $where[] = qsprintf( $conn_r, 'titleSlugHash in (%Ls)', $hashes); } if ($this->contexts) { $with_null = false; $contexts = $this->contexts; foreach ($contexts as $key => $value) { if ($value === null) { unset($contexts[$key]); $with_null = true; continue; } } if ($contexts && $with_null) { $where[] = qsprintf( $conn_r, 'context IN (%Ls) OR context IS NULL', $contexts); } else if ($contexts) { $where[] = qsprintf( $conn_r, 'context IN (%Ls)', $contexts); } else if ($with_null) { $where[] = qsprintf( $conn_r, 'context IS NULL'); } } if ($this->indexes) { $where[] = qsprintf( $conn_r, 'atomIndex IN (%Ld)', $this->indexes); } if (!$this->includeUndocumentable) { $where[] = qsprintf( $conn_r, 'isDocumentable = 1'); } if (!$this->includeGhosts) { $where[] = qsprintf( $conn_r, 'graphHash IS NOT NULL'); } if ($this->nodeHashes) { $where[] = qsprintf( $conn_r, 'nodeHash IN (%Ls)', $this->nodeHashes); } + if ($this->nameContains) { + // NOTE: This CONVERT() call makes queries case-insensitive, since the + // column has binary collation. Eventually, this should move into + // fulltext. + + $where[] = qsprintf( + $conn_r, + 'CONVERT(name USING utf8) LIKE %~', + $this->nameContains); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } /** * Walk a list of atoms and collect all the node hashes of the atoms' * children. When recursing, also walk up the tree and collect children of * atoms they extend. * * @param list List of symbols to collect child hashes of. * @param bool True to collect children of extended atoms, * as well. * @return map Hashes of atoms' children. */ private function getAllChildHashes(array $symbols, $recurse_up) { assert_instances_of($symbols, 'DivinerLiveSymbol'); $hashes = array(); foreach ($symbols as $symbol) { foreach ($symbol->getAtom()->getChildHashes() as $hash) { $hashes[$hash] = $hash; } if ($recurse_up) { $hashes += $this->getAllChildHashes($symbol->getExtends(), true); } } return $hashes; } /** * Attach child atoms to existing atoms. In recursive mode, also attach child * atoms to atoms that these atoms extend. * * @param list List of symbols to attach childeren to. * @param map Map of symbols, keyed by node hash. * @param bool True to attach children to extended atoms, as well. * @return void */ private function attachAllChildren( array $symbols, array $children, $recurse_up) { assert_instances_of($symbols, 'DivinerLiveSymbol'); assert_instances_of($children, 'DivinerLiveSymbol'); foreach ($symbols as $symbol) { $symbol_children = array(); foreach ($symbol->getAtom()->getChildHashes() as $hash) { if (isset($children[$hash])) { $symbol_children[] = $children[$hash]; } } $symbol->attachChildren($symbol_children); if ($recurse_up) { $this->attachAllChildren($symbol->getExtends(), $children, true); } } } public function getQueryApplicationClass() { return 'PhabricatorApplicationDiviner'; } } diff --git a/src/applications/diviner/query/DivinerAtomSearchEngine.php b/src/applications/diviner/query/DivinerAtomSearchEngine.php index 2701e26435..6c8382be3b 100644 --- a/src/applications/diviner/query/DivinerAtomSearchEngine.php +++ b/src/applications/diviner/query/DivinerAtomSearchEngine.php @@ -1,49 +1,91 @@ setParameter( + 'types', + $this->readListFromRequest($request, 'types')); + + $saved->setParameter('name', $request->getStr('name')); + return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new DivinerAtomQuery()); + $types = $saved->getParameter('types'); + if ($types) { + $query->withTypes($types); + } + + $name = $saved->getParameter('name'); + if ($name) { + $query->withNameContains($name); + } + return $query; } public function buildSearchForm( AphrontFormView $form, - PhabricatorSavedQuery $saved_query) { + PhabricatorSavedQuery $saved) { + + $all_types = array(); + foreach (DivinerAtom::getAllTypes() as $type) { + $all_types[$type] = DivinerAtom::getAtomTypeNameString($type); + } + asort($all_types); + + $types = $saved->getParameter('types', array()); + $types = array_fuse($types); + $type_control = id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Types')); + foreach ($all_types as $type => $name) { + $type_control->addCheckbox( + 'types[]', + $type, + $name, + isset($types[$type])); + } + + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name Contains')) + ->setName('name') + ->setValue($saved->getParameter('name'))) + ->appendChild($type_control); } protected function getURI($path) { return '/diviner/'.$path; } public function getBuiltinQueryNames() { $names = array( 'all' => pht('All'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } }