diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index 68a7dee3a3..ebf0b891a1 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -1,1032 +1,1035 @@ authorPHIDs = $authors; return $this; } public function withIDs(array $ids) { $this->taskIDs = $ids; return $this; } public function withPHIDs(array $phids) { $this->taskPHIDs = $phids; return $this; } public function withOwners(array $owners) { $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; $this->includeUnowned = false; foreach ($owners as $k => $phid) { if ($phid === $no_owner || $phid === null) { $this->includeUnowned = true; unset($owners[$k]); break; } } $this->ownerPHIDs = $owners; return $this; } public function withAllProjects(array $projects) { $this->includeNoProject = false; foreach ($projects as $k => $phid) { if ($phid == ManiphestTaskOwner::PROJECT_NO_PROJECT) { $this->includeNoProject = true; unset($projects[$k]); } } $this->projectPHIDs = $projects; return $this; } /** * Add an additional "all projects" constraint to existing filters. * * This is used by boards to supplement queries. * * @param list List of project PHIDs to add to any existing constraint. * @return this */ public function addWithAllProjects(array $projects) { if ($this->projectPHIDs === null) { $this->projectPHIDs = array(); } return $this->withAllProjects(array_merge($this->projectPHIDs, $projects)); } public function withoutProjects(array $projects) { $this->xprojectPHIDs = $projects; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withPriorities(array $priorities) { $this->priorities = $priorities; return $this; } public function withSubpriorities(array $subpriorities) { $this->subpriorities = $subpriorities; return $this; } public function withSubpriorityBetween($min, $max) { $this->subpriorityMin = $min; $this->subpriorityMax = $max; return $this; } public function withSubscribers(array $subscribers) { $this->subscriberPHIDs = $subscribers; return $this; } public function withFullTextSearch($fulltext_search) { $this->fullTextSearch = $fulltext_search; return $this; } public function setGroupBy($group) { $this->groupBy = $group; return $this; } public function setOrderBy($order) { $this->orderBy = $order; return $this; } public function withAnyProjects(array $projects) { $this->anyProjectPHIDs = $projects; return $this; } public function withAnyUserProjects(array $users) { $this->anyUserProjectPHIDs = $users; return $this; } /** * True returns tasks that are blocking other tasks only. * False returns tasks that are not blocking other tasks only. * Null returns tasks regardless of blocking status. */ public function withBlockingTasks($mode) { $this->blockingTasks = $mode; return $this; } public function shouldJoinBlockingTasks() { return $this->blockingTasks !== null; } /** * True returns tasks that are blocked by other tasks only. * False returns tasks that are not blocked by other tasks only. * Null returns tasks regardless of blocked by status. */ public function withBlockedTasks($mode) { $this->blockedTasks = $mode; return $this; } public function shouldJoinBlockedTasks() { return $this->blockedTasks !== null; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withDateModifiedBefore($date_modified_before) { $this->dateModifiedBefore = $date_modified_before; return $this; } public function withDateModifiedAfter($date_modified_after) { $this->dateModifiedAfter = $date_modified_after; return $this; } public function needSubscriberPHIDs($bool) { $this->needSubscriberPHIDs = $bool; return $this; } public function needProjectPHIDs($bool) { $this->needProjectPHIDs = $bool; return $this; } protected function newResultObject() { return new ManiphestTask(); } protected function willExecute() { // Make sure the user can see any projects specified in this // query FIRST. if ($this->projectPHIDs) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($this->projectPHIDs) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($this->projectPHIDs as $index => $phid) { $project = idx($projects, $phid); if (!$project) { unset($this->projectPHIDs[$index]); continue; } } if (!$this->projectPHIDs) { $this->projectPolicyCheckFailed = true; } $this->projectPHIDs = array_values($this->projectPHIDs); } // If we already have an order vector, use it as provided. // TODO: This is a messy hack to make setOrderVector() stronger than // setPriority(). $vector = $this->getOrderVector(); $keys = mpull(iterator_to_array($vector), 'getOrderKey'); if (array_values($keys) !== array('id')) { return; } $parts = array(); switch ($this->groupBy) { case self::GROUP_NONE: break; case self::GROUP_PRIORITY: $parts[] = array('priority'); break; case self::GROUP_OWNER: $parts[] = array('owner'); break; case self::GROUP_STATUS: $parts[] = array('status'); break; case self::GROUP_PROJECT: $parts[] = array('project'); break; } if ($this->applicationSearchOrders) { $columns = array(); foreach ($this->applicationSearchOrders as $order) { $part = 'custom:'.$order['key']; if ($order['ascending']) { $part = '-'.$part; } $columns[] = $part; } $columns[] = 'id'; $parts[] = $columns; } else { switch ($this->orderBy) { case self::ORDER_PRIORITY: $parts[] = array('priority', 'subpriority', 'id'); break; case self::ORDER_CREATED: $parts[] = array('id'); break; case self::ORDER_MODIFIED: $parts[] = array('updated', 'id'); break; case self::ORDER_TITLE: $parts[] = array('title', 'id'); break; } } $parts = array_mergev($parts); // We may have a duplicate column if we are both ordering and grouping // by priority. $parts = array_unique($parts); $this->setOrderVector($parts); } protected function loadPage() { if ($this->projectPolicyCheckFailed) { throw new PhabricatorEmptyQueryException(); } $task_dao = new ManiphestTask(); $conn = $task_dao->establishConnection('r'); $where = array(); $where[] = $this->buildTaskIDsWhereClause($conn); $where[] = $this->buildTaskPHIDsWhereClause($conn); $where[] = $this->buildStatusWhereClause($conn); $where[] = $this->buildStatusesWhereClause($conn); $where[] = $this->buildDependenciesWhereClause($conn); $where[] = $this->buildAuthorWhereClause($conn); $where[] = $this->buildOwnerWhereClause($conn); $where[] = $this->buildProjectWhereClause($conn); $where[] = $this->buildAnyProjectWhereClause($conn); $where[] = $this->buildAnyUserProjectWhereClause($conn); $where[] = $this->buildXProjectWhereClause($conn); $where[] = $this->buildFullTextWhereClause($conn); if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn, 'task.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn, 'task.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->dateModifiedAfter) { $where[] = qsprintf( $conn, 'task.dateModified >= %d', $this->dateModifiedAfter); } if ($this->dateModifiedBefore) { $where[] = qsprintf( $conn, 'task.dateModified <= %d', $this->dateModifiedBefore); } if ($this->priorities) { $where[] = qsprintf( $conn, 'task.priority IN (%Ld)', $this->priorities); } if ($this->subpriorities) { $where[] = qsprintf( $conn, 'task.subpriority IN (%Lf)', $this->subpriorities); } if ($this->subpriorityMin) { $where[] = qsprintf( $conn, 'task.subpriority >= %f', $this->subpriorityMin); } if ($this->subpriorityMax) { $where[] = qsprintf( $conn, 'task.subpriority <= %f', $this->subpriorityMax); } - $where[] = $this->buildPagingClause($conn); + $where[] = $this->buildWhereClauseParts($conn); $where = $this->formatWhereClause($where); - $having = ''; $count = ''; - if (count($this->projectPHIDs) > 1) { - // We want to treat the query as an intersection query, not a union - // query. We sum the project count and require it be the same as the - // number of projects we're searching for. - $count = ', COUNT(project.dst) projectCount'; - $having = qsprintf( - $conn, - 'HAVING projectCount = %d', - count($this->projectPHIDs)); } $group_column = ''; switch ($this->groupBy) { case self::GROUP_PROJECT: $group_column = qsprintf( $conn, ', projectGroupName.indexedObjectPHID projectGroupPHID'); break; } $rows = queryfx_all( $conn, - 'SELECT task.* %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', + '%Q %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', + $this->buildSelectClause($conn), $count, $group_column, $task_dao->getTableName(), - $this->buildJoinsClause($conn), + $this->buildJoinClause($conn), $where, $this->buildGroupClause($conn), - $having, + $this->buildHavingClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); switch ($this->groupBy) { case self::GROUP_PROJECT: $data = ipull($rows, null, 'id'); break; default: $data = $rows; break; } $tasks = $task_dao->loadAllFromArray($data); switch ($this->groupBy) { case self::GROUP_PROJECT: $results = array(); foreach ($rows as $row) { $task = clone $tasks[$row['id']]; $task->attachGroupByProjectPHID($row['projectGroupPHID']); $results[] = $task; } $tasks = $results; break; } return $tasks; } protected function willFilterPage(array $tasks) { if ($this->groupBy == self::GROUP_PROJECT) { // We should only return project groups which the user can actually see. $project_phids = mpull($tasks, 'getGroupByProjectPHID'); $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($tasks as $key => $task) { if (!$task->getGroupByProjectPHID()) { // This task is either not in any projects, or only in projects // which we're ignoring because they're being queried for explicitly. continue; } if (empty($projects[$task->getGroupByProjectPHID()])) { unset($tasks[$key]); } } } return $tasks; } protected function didFilterPage(array $tasks) { $phids = mpull($tasks, 'getPHID'); if ($this->needProjectPHIDs) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($tasks as $task) { $project_phids = $edge_query->getDestinationPHIDs( array($task->getPHID())); $task->attachProjectPHIDs($project_phids); } } if ($this->needSubscriberPHIDs) { $subscriber_sets = id(new PhabricatorSubscribersQuery()) ->withObjectPHIDs($phids) ->execute(); foreach ($tasks as $task) { $subscribers = idx($subscriber_sets, $task->getPHID(), array()); $task->attachSubscriberPHIDs($subscribers); } } return $tasks; } private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) { if (!$this->taskIDs) { return null; } return qsprintf( $conn, 'task.id in (%Ld)', $this->taskIDs); } private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) { if (!$this->taskPHIDs) { return null; } return qsprintf( $conn, 'task.phid in (%Ls)', $this->taskPHIDs); } private function buildStatusWhereClause(AphrontDatabaseConnection $conn) { static $map = array( self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID, self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE, self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE, ); switch ($this->status) { case self::STATUS_ANY: return null; case self::STATUS_OPEN: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); case self::STATUS_CLOSED: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getClosedStatusConstants()); default: $constant = idx($map, $this->status); if (!$constant) { throw new Exception("Unknown status query '{$this->status}'!"); } return qsprintf( $conn, 'task.status = %s', $constant); } } private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) { if ($this->statuses) { return qsprintf( $conn, 'task.status IN (%Ls)', $this->statuses); } return null; } private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) { if (!$this->authorPHIDs) { return null; } return qsprintf( $conn, 'task.authorPHID in (%Ls)', $this->authorPHIDs); } private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) { if (!$this->ownerPHIDs) { if ($this->includeUnowned === null) { return null; } else if ($this->includeUnowned) { return qsprintf( $conn, 'task.ownerPHID IS NULL'); } else { return qsprintf( $conn, 'task.ownerPHID IS NOT NULL'); } } if ($this->includeUnowned) { return qsprintf( $conn, 'task.ownerPHID IN (%Ls) OR task.ownerPHID IS NULL', $this->ownerPHIDs); } else { return qsprintf( $conn, 'task.ownerPHID IN (%Ls)', $this->ownerPHIDs); } } private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) { if (!strlen($this->fullTextSearch)) { return null; } // In doing a fulltext search, we first find all the PHIDs that match the // fulltext search, and then use that to limit the rest of the search $fulltext_query = id(new PhabricatorSavedQuery()) ->setEngineClassName('PhabricatorSearchApplicationSearchEngine') ->setParameter('query', $this->fullTextSearch); // NOTE: Setting this to something larger than 2^53 will raise errors in // ElasticSearch, and billions of results won't fit in memory anyway. $fulltext_query->setParameter('limit', 100000); $fulltext_query->setParameter('type', ManiphestTaskPHIDType::TYPECONST); $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); $fulltext_results = $engine->executeSearch($fulltext_query); if (empty($fulltext_results)) { $fulltext_results = array(null); } return qsprintf( $conn, 'task.phid IN (%Ls)', $fulltext_results); } private function buildDependenciesWhereClause( AphrontDatabaseConnection $conn) { if (!$this->shouldJoinBlockedTasks() && !$this->shouldJoinBlockingTasks()) { return null; } $parts = array(); if ($this->blockingTasks === true) { $parts[] = qsprintf( $conn, 'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockingTasks === false) { $parts[] = qsprintf( $conn, 'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } if ($this->blockedTasks === true) { $parts[] = qsprintf( $conn, 'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockedTasks === false) { $parts[] = qsprintf( $conn, 'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } return '('.implode(') OR (', $parts).')'; } private function buildProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->projectPHIDs && !$this->includeNoProject) { return null; } $parts = array(); if ($this->projectPHIDs) { $parts[] = qsprintf( $conn, 'project.dst in (%Ls)', $this->projectPHIDs); } if ($this->includeNoProject) { $parts[] = qsprintf( $conn, 'project.dst IS NULL'); } return '('.implode(') OR (', $parts).')'; } private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->anyProjectPHIDs) { return null; } return qsprintf( $conn, 'anyproject.dst IN (%Ls)', $this->anyProjectPHIDs); } private function buildAnyUserProjectWhereClause( AphrontDatabaseConnection $conn) { if (!$this->anyUserProjectPHIDs) { return null; } $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($this->anyUserProjectPHIDs) ->execute(); $any_user_project_phids = mpull($projects, 'getPHID'); if (!$any_user_project_phids) { throw new PhabricatorEmptyQueryException(); } return qsprintf( $conn, 'anyproject.dst IN (%Ls)', $any_user_project_phids); } private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->xprojectPHIDs) { return null; } return qsprintf( $conn, 'xproject.dst IS NULL'); } - private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; $joins = array(); if ($this->projectPHIDs || $this->includeNoProject) { $joins[] = qsprintf( $conn_r, '%Q JOIN %T project ON project.src = task.phid AND project.type = %d', ($this->includeNoProject ? 'LEFT' : ''), $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } if ($this->shouldJoinBlockingTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocking ON blocking.src = task.phid '. 'AND blocking.type = %d '. 'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid', $edge_table, ManiphestTaskDependedOnByTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->shouldJoinBlockedTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocked ON blocked.src = task.phid '. 'AND blocked.type = %d '. 'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid', $edge_table, ManiphestTaskDependsOnTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T anyproject ON anyproject.src = task.phid AND anyproject.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } if ($this->xprojectPHIDs) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T xproject ON xproject.src = task.phid AND xproject.type = %d AND xproject.dst IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $this->xprojectPHIDs); } if ($this->subscriberPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T e_ccs ON e_ccs.src = task.phid '. 'AND e_ccs.type = %s '. 'AND e_ccs.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasSubscriberEdgeType::EDGECONST, $this->subscriberPHIDs); } switch ($this->groupBy) { case self::GROUP_PROJECT: $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs(); if ($ignore_group_phids) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d AND projectGroup.dst NOT IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $ignore_group_phids); } else { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroupName ON projectGroup.dst = projectGroupName.indexedObjectPHID', id(new ManiphestNameIndex())->getTableName()); break; } - $joins[] = $this->buildApplicationSearchJoinClause($conn_r); + $joins[] = parent::buildJoinClauseParts($conn_r); - return implode(' ', $joins); + return $joins; } protected function buildGroupClause(AphrontDatabaseConnection $conn_r) { $joined_multiple_rows = (count($this->projectPHIDs) > 1) || (count($this->anyProjectPHIDs) > 1) || $this->shouldJoinBlockingTasks() || $this->shouldJoinBlockedTasks() || - ($this->getApplicationSearchMayJoinMultipleRows()); + ($this->shouldGroupQueryResultRows()); $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); // If we're joining multiple rows, we need to group the results by the // task IDs. if ($joined_multiple_rows) { if ($joined_project_name) { return 'GROUP BY task.phid, projectGroup.dst'; } else { return 'GROUP BY task.phid'; } } else { return ''; } } /** * Return project PHIDs which we should ignore when grouping tasks by * project. For example, if a user issues a query like: * * Tasks in all projects: Frontend, Bugs * * ...then we don't show "Frontend" or "Bugs" groups in the result set, since * they're meaningless as all results are in both groups. * * Similarly, for queries like: * * Tasks in any projects: Public Relations * * ...we ignore the single project, as every result is in that project. (In * the case that there are several "any" projects, we do not ignore them.) * * @return list Project PHIDs which should be ignored in query * construction. */ private function getIgnoreGroupedProjectPHIDs() { $phids = array(); if ($this->projectPHIDs) { $phids[] = $this->projectPHIDs; } if (count($this->anyProjectPHIDs) == 1) { $phids[] = $this->anyProjectPHIDs; } // Maybe we should also exclude the "excludeProjectPHIDs"? It won't // impact the results, but we might end up with a better query plan. // Investigate this on real data? This is likely very rare. return array_mergev($phids); } + // TODO: Remove this when moving fully to edge logic. + protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) { + $having = parent::buildHavingClauseParts($conn); + if (count($this->projectPHIDs) > 1) { + $having[] = qsprintf( + $conn, + 'projectCount = %d', + count($this->projectPHIDs)); + } + return $having; + } + protected function getResultCursor($result) { $id = $result->getID(); if ($this->groupBy == self::GROUP_PROJECT) { return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');; } return $id; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'priority' => array( 'table' => 'task', 'column' => 'priority', 'type' => 'int', ), 'owner' => array( 'table' => 'task', 'column' => 'ownerOrdering', 'null' => 'head', 'reverse' => true, 'type' => 'string', ), 'status' => array( 'table' => 'task', 'column' => 'status', 'type' => 'string', 'reverse' => true, ), 'project' => array( 'table' => 'projectGroupName', 'column' => 'indexedObjectName', 'type' => 'string', 'null' => 'head', 'reverse' => true, ), 'title' => array( 'table' => 'task', 'column' => 'title', 'type' => 'string', 'reverse' => true, ), 'subpriority' => array( 'table' => 'task', 'column' => 'subpriority', 'type' => 'float', ), 'updated' => array( 'table' => 'task', 'column' => 'dateModified', 'type' => 'int', ), ); } protected function getPagingValueMap($cursor, array $keys) { $cursor_parts = explode('.', $cursor, 2); $task_id = $cursor_parts[0]; $group_id = idx($cursor_parts, 1); $task = $this->loadCursorObject($task_id); $map = array( 'id' => $task->getID(), 'priority' => $task->getPriority(), 'subpriority' => $task->getSubpriority(), 'owner' => $task->getOwnerOrdering(), 'status' => $task->getStatus(), 'title' => $task->getTitle(), 'updated' => $task->getDateModified(), ); foreach ($keys as $key) { switch ($key) { case 'project': $value = null; if ($group_id) { $paging_projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($group_id)) ->execute(); if ($paging_projects) { $value = head($paging_projects)->getName(); } } $map[$key] = $value; break; } } foreach ($keys as $key) { if ($this->isCustomFieldOrderKey($key)) { $map += $this->getPagingValueMapForCustomFields($task); break; } } return $map; } protected function getPrimaryTableAlias() { return 'task'; } public function getQueryApplicationClass() { return 'PhabricatorManiphestApplication'; } } diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index d47297d456..6edf7badf6 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -1,569 +1,520 @@ isBoardView = $is_board_view; return $this; } public function getIsBoardView() { return $this->isBoardView; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setShowBatchControls($show_batch_controls) { $this->showBatchControls = $show_batch_controls; return $this; } public function getResultTypeDescription() { return pht('Tasks'); } public function getApplicationClassName() { return 'PhabricatorManiphestApplication'; } public function getCustomFieldObject() { return new ManiphestTask(); } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter( 'assignedPHIDs', $this->readUsersFromRequest($request, 'assigned')); $saved->setParameter( 'authorPHIDs', $this->readUsersFromRequest($request, 'authors')); $saved->setParameter( 'subscriberPHIDs', $this->readPHIDsFromRequest($request, 'subscribers')); $saved->setParameter( 'statuses', $this->readListFromRequest($request, 'statuses')); $saved->setParameter( 'priorities', $this->readListFromRequest($request, 'priorities')); $saved->setParameter( 'blocking', $this->readBoolFromRequest($request, 'blocking')); $saved->setParameter( 'blocked', $this->readBoolFromRequest($request, 'blocked')); $saved->setParameter('group', $request->getStr('group')); $saved->setParameter('order', $request->getStr('order')); $ids = $request->getStrList('ids'); foreach ($ids as $key => $id) { $id = trim($id, ' Tt'); if (!$id || !is_numeric($id)) { unset($ids[$key]); } else { $ids[$key] = $id; } } $saved->setParameter('ids', $ids); $saved->setParameter('fulltext', $request->getStr('fulltext')); $saved->setParameter( - 'allProjectPHIDs', - $this->readPHIDsFromRequest($request, 'allProjects')); - - $saved->setParameter( - 'withNoProject', - $request->getBool('withNoProject')); - - $saved->setParameter( - 'anyProjectPHIDs', - $this->readPHIDsFromRequest($request, 'anyProjects')); - - $saved->setParameter( - 'excludeProjectPHIDs', - $this->readPHIDsFromRequest($request, 'excludeProjects')); - - $saved->setParameter( - 'userProjectPHIDs', - $this->readUsersFromRequest($request, 'userProjects')); + 'projects', + $this->readProjectsFromRequest($request, 'projects')); $saved->setParameter('createdStart', $request->getStr('createdStart')); $saved->setParameter('createdEnd', $request->getStr('createdEnd')); $saved->setParameter('modifiedStart', $request->getStr('modifiedStart')); $saved->setParameter('modifiedEnd', $request->getStr('modifiedEnd')); $limit = $request->getInt('limit'); if ($limit > 0) { $saved->setParameter('limit', $limit); } $this->readCustomFieldsFromRequest($request, $saved); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new ManiphestTaskQuery()) ->needProjectPHIDs(true); $author_phids = $saved->getParameter('authorPHIDs'); if ($author_phids) { $query->withAuthors($author_phids); } $subscriber_phids = $saved->getParameter('subscriberPHIDs'); if ($subscriber_phids) { $query->withSubscribers($subscriber_phids); } $datasource = id(new PhabricatorPeopleOwnerDatasource()) ->setViewer($this->requireViewer()); $assigned_phids = $this->readAssignedPHIDs($saved); $assigned_phids = $datasource->evaluateTokens($assigned_phids); if ($assigned_phids) { $query->withOwners($assigned_phids); } $statuses = $saved->getParameter('statuses'); if ($statuses) { $query->withStatuses($statuses); } $priorities = $saved->getParameter('priorities'); if ($priorities) { $query->withPriorities($priorities); } $query->withBlockingTasks($saved->getParameter('blocking')); $query->withBlockedTasks($saved->getParameter('blocked')); $this->applyOrderByToQuery( $query, $this->getOrderValues(), $saved->getParameter('order')); $group = $saved->getParameter('group'); $group = idx($this->getGroupValues(), $group); if ($group) { $query->setGroupBy($group); } else { $query->setGroupBy(head($this->getGroupValues())); } $ids = $saved->getParameter('ids'); if ($ids) { $query->withIDs($ids); } $fulltext = $saved->getParameter('fulltext'); if (strlen($fulltext)) { $query->withFullTextSearch($fulltext); } - $with_no_project = $saved->getParameter('withNoProject'); - if ($with_no_project) { - $query->withAllProjects(array(ManiphestTaskOwner::PROJECT_NO_PROJECT)); - } else { - $project_phids = $saved->getParameter('allProjectPHIDs'); - if ($project_phids) { - $query->withAllProjects($project_phids); - } - } - - $any_project_phids = $saved->getParameter('anyProjectPHIDs'); - if ($any_project_phids) { - $query->withAnyProjects($any_project_phids); - } - - $exclude_project_phids = $saved->getParameter('excludeProjectPHIDs'); - if ($exclude_project_phids) { - $query->withoutProjects($exclude_project_phids); - } - - $user_project_phids = $saved->getParameter('userProjectPHIDs'); - if ($user_project_phids) { - $query->withAnyUserProjects($user_project_phids); - } + $projects = $this->readProjectTokens($saved); + $adjusted = id(clone $saved)->setParameter('projects', $projects); + $this->setQueryProjects($query, $adjusted); $start = $this->parseDateTime($saved->getParameter('createdStart')); $end = $this->parseDateTime($saved->getParameter('createdEnd')); if ($start) { $query->withDateCreatedAfter($start); } if ($end) { $query->withDateCreatedBefore($end); } $mod_start = $this->parseDateTime($saved->getParameter('modifiedStart')); $mod_end = $this->parseDateTime($saved->getParameter('modifiedEnd')); if ($mod_start) { $query->withDateModifiedAfter($mod_start); } if ($mod_end) { $query->withDateModifiedBefore($mod_end); } $this->applyCustomFieldsToQuery($query, $saved); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $assigned_phids = $this->readAssignedPHIDs($saved); $author_phids = $saved->getParameter('authorPHIDs', array()); - $all_project_phids = $saved->getParameter( - 'allProjectPHIDs', - array()); - $any_project_phids = $saved->getParameter( - 'anyProjectPHIDs', - array()); - $exclude_project_phids = $saved->getParameter( - 'excludeProjectPHIDs', - array()); - $user_project_phids = $saved->getParameter( - 'userProjectPHIDs', - array()); - $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); + $projects = $this->readProjectTokens($saved); - $with_no_projects = $saved->getParameter('withNoProject'); + $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); $statuses = $saved->getParameter('statuses', array()); $statuses = array_fuse($statuses); $status_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Status')); foreach (ManiphestTaskStatus::getTaskStatusMap() as $status => $name) { $status_control->addCheckbox( 'statuses[]', $status, $name, isset($statuses[$status])); } $priorities = $saved->getParameter('priorities', array()); $priorities = array_fuse($priorities); $priority_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Priority')); foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $name) { $priority_control->addCheckbox( 'priorities[]', $pri, $name, isset($priorities[$pri])); } $blocking_control = id(new AphrontFormSelectControl()) ->setLabel(pht('Blocking')) ->setName('blocking') ->setValue($this->getBoolFromQuery($saved, 'blocking')) ->setOptions(array( '' => pht('Show All Tasks'), 'true' => pht('Show Tasks Blocking Other Tasks'), 'false' => pht('Show Tasks Not Blocking Other Tasks'), )); $blocked_control = id(new AphrontFormSelectControl()) ->setLabel(pht('Blocked')) ->setName('blocked') ->setValue($this->getBoolFromQuery($saved, 'blocked')) ->setOptions(array( '' => pht('Show All Tasks'), 'true' => pht('Show Tasks Blocked By Other Tasks'), 'false' => pht('Show Tasks Not Blocked By Other Tasks'), )); $ids = $saved->getParameter('ids', array()); $builtin_orders = $this->getOrderOptions(); $custom_orders = $this->getCustomFieldOrderOptions(); $all_orders = $builtin_orders + $custom_orders; $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleOwnerDatasource()) ->setName('assigned') ->setLabel(pht('Assigned To')) ->setValue($assigned_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorProjectDatasource()) - ->setName('allProjects') - ->setLabel(pht('In All Projects')) - ->setValue($all_project_phids)); - - if (!$this->getIsBoardView()) { - $form - ->appendChild( - id(new AphrontFormCheckboxControl()) - ->addCheckbox( - 'withNoProject', - 1, - pht('Show only tasks with no projects.'), - $with_no_projects)); - } - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorProjectDatasource()) - ->setName('anyProjects') - ->setLabel(pht('In Any Project')) - ->setValue($any_project_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorProjectDatasource()) - ->setName('excludeProjects') - ->setLabel(pht('Not In Projects')) - ->setValue($exclude_project_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->setName('userProjects') - ->setLabel(pht('In Users\' Projects')) - ->setValue($user_project_phids)) + ->setDatasource(new PhabricatorProjectLogicalDatasource()) + ->setName('projects') + ->setLabel(pht('Projects')) + ->setValue($projects)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('authors') ->setLabel(pht('Authors')) ->setValue($author_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()) ->setName('subscribers') ->setLabel(pht('Subscribers')) ->setValue($subscriber_phids)) ->appendChild( id(new AphrontFormTextControl()) ->setName('fulltext') ->setLabel(pht('Contains Words')) ->setValue($saved->getParameter('fulltext'))) ->appendChild($status_control) ->appendChild($priority_control) ->appendChild($blocking_control) ->appendChild($blocked_control); if (!$this->getIsBoardView()) { $form ->appendChild( id(new AphrontFormSelectControl()) ->setName('group') ->setLabel(pht('Group By')) ->setValue($saved->getParameter('group')) ->setOptions($this->getGroupOptions())) ->appendChild( id(new AphrontFormSelectControl()) ->setName('order') ->setLabel(pht('Order By')) ->setValue($saved->getParameter('order')) ->setOptions($all_orders)); } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('ids') ->setLabel(pht('Task IDs')) ->setValue(implode(', ', $ids))); $this->appendCustomFieldsToForm($form, $saved); $this->buildDateRange( $form, $saved, 'createdStart', pht('Created After'), 'createdEnd', pht('Created Before')); $this->buildDateRange( $form, $saved, 'modifiedStart', pht('Updated After'), 'modifiedEnd', pht('Updated Before')); if (!$this->getIsBoardView()) { $form ->appendChild( id(new AphrontFormTextControl()) ->setName('limit') ->setLabel(pht('Page Size')) ->setValue($saved->getParameter('limit', 100))); } } protected function getURI($path) { if ($this->baseURI) { return $this->baseURI.$path; } return '/maniphest/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['assigned'] = pht('Assigned'); $names['authored'] = pht('Authored'); $names['subscribed'] = pht('Subscribed'); } $names['open'] = pht('Open Tasks'); $names['all'] = pht('All Tasks'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer_phid = $this->requireViewer()->getPHID(); switch ($query_key) { case 'all': return $query; case 'assigned': return $query ->setParameter('assignedPHIDs', array($viewer_phid)) ->setParameter( 'statuses', ManiphestTaskStatus::getOpenStatusConstants()); case 'subscribed': return $query ->setParameter('subscriberPHIDs', array($viewer_phid)) ->setParameter( 'statuses', ManiphestTaskStatus::getOpenStatusConstants()); case 'open': return $query ->setParameter( 'statuses', ManiphestTaskStatus::getOpenStatusConstants()); case 'authored': return $query ->setParameter('authorPHIDs', array($viewer_phid)) ->setParameter('order', 'created') ->setParameter('group', 'none'); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getOrderOptions() { return array( 'priority' => pht('Priority'), 'updated' => pht('Date Updated'), 'created' => pht('Date Created'), 'title' => pht('Title'), ); } private function getOrderValues() { return array( 'priority' => ManiphestTaskQuery::ORDER_PRIORITY, 'updated' => ManiphestTaskQuery::ORDER_MODIFIED, 'created' => ManiphestTaskQuery::ORDER_CREATED, 'title' => ManiphestTaskQuery::ORDER_TITLE, ); } private function getGroupOptions() { return array( 'priority' => pht('Priority'), 'assigned' => pht('Assigned'), 'status' => pht('Status'), 'project' => pht('Project'), 'none' => pht('None'), ); } private function getGroupValues() { return array( 'priority' => ManiphestTaskQuery::GROUP_PRIORITY, 'assigned' => ManiphestTaskQuery::GROUP_OWNER, 'status' => ManiphestTaskQuery::GROUP_STATUS, 'project' => ManiphestTaskQuery::GROUP_PROJECT, 'none' => ManiphestTaskQuery::GROUP_NONE, ); } protected function renderResultList( array $tasks, PhabricatorSavedQuery $saved, array $handles) { $viewer = $this->requireViewer(); if ($this->isPanelContext()) { $can_edit_priority = false; $can_bulk_edit = false; } else { $can_edit_priority = PhabricatorPolicyFilter::hasCapability( $viewer, $this->getApplication(), ManiphestEditPriorityCapability::CAPABILITY); $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $this->getApplication(), ManiphestBulkEditCapability::CAPABILITY); } return id(new ManiphestTaskResultListView()) ->setUser($viewer) ->setTasks($tasks) ->setSavedQuery($saved) ->setCanEditPriority($can_edit_priority) ->setCanBatchEdit($can_bulk_edit) ->setShowBatchControls($this->showBatchControls); } private function readAssignedPHIDs(PhabricatorSavedQuery $saved) { $assigned_phids = $saved->getParameter('assignedPHIDs', array()); // This may be present in old saved queries from before parameterized // typeaheads, and is retained for compatibility. We could remove it by // migrating old saved queries. if ($saved->getParameter('withUnassigned')) { $assigned_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; } return $assigned_phids; } + private function readProjectTokens(PhabricatorSavedQuery $saved) { + $projects = $saved->getParameter('projects', array()); + + $all = $saved->getParameter('allProjectPHIDs', array()); + foreach ($all as $phid) { + $projects[] = $phid; + } + + $any = $saved->getParameter('anyProjectPHIDs', array()); + foreach ($any as $phid) { + $projects[] = 'any('.$phid.')'; + } + + $not = $saved->getParameter('excludeProjectPHIDs', array()); + foreach ($not as $phid) { + $projects[] = 'not('.$phid.')'; + } + + $users = $saved->getParameter('userProjectPHIDs', array()); + foreach ($users as $phid) { + $projects[] = 'projects('.$phid.')'; + } + + $no = $saved->getParameter('withNoProject'); + if ($no) { + $projects[] = 'null()'; + } + + return $projects; + } + } diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php index 2efb1b4eb6..ad392a677a 100644 --- a/src/applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php @@ -1,163 +1,165 @@ ) or not()...'); } public function getDatasourceApplicationClass() { return 'PhabricatorProjectApplication'; } public function getComponentDatasources() { return array( new PhabricatorProjectDatasource(), ); } public function getDatasourceFunctions() { return array( 'any' => array( 'name' => pht('In Any: ...'), 'arguments' => pht('project'), 'summary' => pht('Find results in any of several projects.'), 'description' => pht( 'This function allows you to find results in one of several '. 'projects. Another way to think of this function is that it '. 'allows you to perform an "or" query.'. "\n\n". 'By default, if you enter several projects, results are returned '. 'only if they belong to all of the projects you enter. That is, '. 'this query will only return results in //both// projects:'. "\n\n". '> ios, android'. "\n\n". 'If you want to find results in any of several projects, you can '. 'use the `any()` function. For example, you can use this query to '. 'find results which are in //either// project:'. "\n\n". '> any(ios), any(android)'. "\n\n". 'You can combine the `any()` function with normal project tokens '. 'to refine results. For example, use this query to find bugs in '. '//either// iOS or Android:'. "\n\n". '> bug, any(ios), any(android)'), ), 'not' => array( 'name' => pht('Not In: ...'), 'arguments' => pht('project'), 'summary' => pht('Find results not in specific projects.'), 'description' => pht( 'This function allows you to find results which are not in '. 'one or more projects. For example, use this query to find '. 'results which are not associated with a specific project:'. "\n\n". '> not(vanilla)'. "\n\n". 'You can exclude multiple projects. This will cause the query '. 'to return only results which are not in any of the excluded '. 'projects:'. "\n\n". '> not(vanilla), not(chocolate)'. "\n\n". 'You can combine this function with other functions to refine '. 'results. For example, use this query to find iOS results which '. 'are not bugs:'. "\n\n". '> ios, not(bug)'), ), ); } protected function didLoadResults(array $results) { $function = $this->getCurrentFunction(); $return_any = ($function !== 'not'); $return_not = ($function !== 'any'); $return = array(); foreach ($results as $result) { $result ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) - ->setIcon('fa-asterisk'); + ->setIcon('fa-asterisk') + ->setColor(null); if ($return_any) { $return[] = id(clone $result) ->setPHID('any('.$result->getPHID().')') ->setDisplayName(pht('In Any: %s', $result->getDisplayName())) ->setName($result->getName().' any'); } if ($return_not) { $return[] = id(clone $result) ->setPHID('not('.$result->getPHID().')') ->setDisplayName(pht('Not In: %s', $result->getDisplayName())) ->setName($result->getName().' not'); } } return $return; } protected function evaluateFunction($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $operator = array( 'any' => PhabricatorQueryConstraint::OPERATOR_OR, 'not' => PhabricatorQueryConstraint::OPERATOR_NOT, ); $results = array(); foreach ($phids as $phid) { $results[] = new PhabricatorQueryConstraint( $operator[$function], $phid); } return $results; } public function renderFunctionTokens($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $tokens = $this->renderTokens($phids); foreach ($tokens as $token) { + $token->setColor(null); if ($token->isInvalid()) { if ($function == 'any') { $token->setValue(pht('In Any: Invalid Project')); } else { $token->setValue(pht('Not In: Invalid Project')); } } else { $token ->setIcon('fa-asterisk') ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION); if ($function == 'any') { $token ->setKey('any('.$token->getKey().')') ->setValue(pht('In Any: %s', $token->getValue())); } else { $token ->setKey('not('.$token->getKey().')') ->setValue(pht('Not In: %s', $token->getValue())); } } } return $tokens; } } diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php index 8d1542d9ca..d2cb58a7ee 100644 --- a/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php @@ -1,137 +1,139 @@ )...'); } public function getDatasourceApplicationClass() { return 'PhabricatorProjectApplication'; } public function getComponentDatasources() { return array( new PhabricatorPeopleDatasource(), ); } public function getDatasourceFunctions() { return array( 'projects' => array( 'name' => pht('Projects: ...'), 'arguments' => pht('username'), 'summary' => pht("Find results in any of a user's projects."), 'description' => pht( 'This function allows you to find results associated with any '. 'of the projects a specified user is a member of. For example, '. 'this will find results associated with all of the projects '. '`alincoln` is a member of:'. "\n\n". '> projects(alincoln)'. "\n\n"), ), ); } protected function didLoadResults(array $results) { foreach ($results as $result) { $result + ->setColor(null) ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setIcon('fa-asterisk') ->setPHID('projects('.$result->getPHID().')') ->setDisplayName(pht("User's Projects: %s", $result->getDisplayName())) ->setName($result->getName().' projects'); } return $results; } protected function evaluateFunction($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $phids = $this->resolvePHIDs($phids); $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($phids) ->execute(); $results = array(); foreach ($projects as $project) { $results[] = new PhabricatorQueryConstraint( PhabricatorQueryConstraint::OPERATOR_OR, $project->getPHID()); } return $results; } public function renderFunctionTokens($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $phids = $this->resolvePHIDs($phids); $tokens = $this->renderTokens($phids); foreach ($tokens as $token) { + $token->setColor(null); if ($token->isInvalid()) { $token ->setValue(pht("User's Projects: Invalid User")); } else { $token ->setIcon('fa-asterisk') ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setKey('projects('.$token->getKey().')') ->setValue(pht("User's Projects: %s", $token->getValue())); } } return $tokens; } private function resolvePHIDs(array $phids) { // If we have a function like `projects(alincoln)`, try to resolve the // username first. This won't happen normally, but can be passed in from // the query string. // The user might also give us an invalid username. In this case, we // preserve it and return it in-place so we get an "invalid" token rendered // in the UI. This shows the user where the issue is and best represents // the user's input. $usernames = array(); foreach ($phids as $key => $phid) { if (phid_get_type($phid) != PhabricatorPeopleUserPHIDType::TYPECONST) { $usernames[$key] = $phid; } } if ($usernames) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getViewer()) ->withUsernames($usernames) ->execute(); $users = mpull($users, null, 'getUsername'); foreach ($usernames as $key => $username) { $user = idx($users, $username); if ($user) { $phids[$key] = $user->getPHID(); } } } return $phids; } }