Path: blob/master/src/applications/maniphest/query/ManiphestTaskQuery.php
12262 views
<?php12/**3* Query tasks by specific criteria. This class uses the higher-performance4* but less-general Maniphest indexes to satisfy queries.5*/6final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {78private $taskIDs;9private $taskPHIDs;10private $authorPHIDs;11private $ownerPHIDs;12private $noOwner;13private $anyOwner;14private $subscriberPHIDs;15private $dateCreatedAfter;16private $dateCreatedBefore;17private $dateModifiedAfter;18private $dateModifiedBefore;19private $bridgedObjectPHIDs;20private $hasOpenParents;21private $hasOpenSubtasks;22private $parentTaskIDs;23private $subtaskIDs;24private $subtypes;25private $closedEpochMin;26private $closedEpochMax;27private $closerPHIDs;28private $columnPHIDs;29private $specificGroupByProjectPHID;3031private $status = 'status-any';32const STATUS_ANY = 'status-any';33const STATUS_OPEN = 'status-open';34const STATUS_CLOSED = 'status-closed';35const STATUS_RESOLVED = 'status-resolved';36const STATUS_WONTFIX = 'status-wontfix';37const STATUS_INVALID = 'status-invalid';38const STATUS_SPITE = 'status-spite';39const STATUS_DUPLICATE = 'status-duplicate';4041private $statuses;42private $priorities;43private $subpriorities;4445private $groupBy = 'group-none';46const GROUP_NONE = 'group-none';47const GROUP_PRIORITY = 'group-priority';48const GROUP_OWNER = 'group-owner';49const GROUP_STATUS = 'group-status';50const GROUP_PROJECT = 'group-project';5152const ORDER_PRIORITY = 'order-priority';53const ORDER_CREATED = 'order-created';54const ORDER_MODIFIED = 'order-modified';55const ORDER_TITLE = 'order-title';5657private $needSubscriberPHIDs;58private $needProjectPHIDs;5960public function withAuthors(array $authors) {61$this->authorPHIDs = $authors;62return $this;63}6465public function withIDs(array $ids) {66$this->taskIDs = $ids;67return $this;68}6970public function withPHIDs(array $phids) {71$this->taskPHIDs = $phids;72return $this;73}7475public function withOwners(array $owners) {76if ($owners === array()) {77throw new Exception(pht('Empty withOwners() constraint is not valid.'));78}7980$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;81$any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;8283foreach ($owners as $k => $phid) {84if ($phid === $no_owner || $phid === null) {85$this->noOwner = true;86unset($owners[$k]);87break;88}89if ($phid === $any_owner) {90$this->anyOwner = true;91unset($owners[$k]);92break;93}94}9596if ($owners) {97$this->ownerPHIDs = $owners;98}99100return $this;101}102103public function withStatus($status) {104$this->status = $status;105return $this;106}107108public function withStatuses(array $statuses) {109$this->statuses = $statuses;110return $this;111}112113public function withPriorities(array $priorities) {114$this->priorities = $priorities;115return $this;116}117118public function withSubpriorities(array $subpriorities) {119$this->subpriorities = $subpriorities;120return $this;121}122123public function withSubscribers(array $subscribers) {124$this->subscriberPHIDs = $subscribers;125return $this;126}127128public function setGroupBy($group) {129$this->groupBy = $group;130131switch ($this->groupBy) {132case self::GROUP_NONE:133$vector = array();134break;135case self::GROUP_PRIORITY:136$vector = array('priority');137break;138case self::GROUP_OWNER:139$vector = array('owner');140break;141case self::GROUP_STATUS:142$vector = array('status');143break;144case self::GROUP_PROJECT:145$vector = array('project');146break;147}148149$this->setGroupVector($vector);150151return $this;152}153154public function withOpenSubtasks($value) {155$this->hasOpenSubtasks = $value;156return $this;157}158159public function withOpenParents($value) {160$this->hasOpenParents = $value;161return $this;162}163164public function withParentTaskIDs(array $ids) {165$this->parentTaskIDs = $ids;166return $this;167}168169public function withSubtaskIDs(array $ids) {170$this->subtaskIDs = $ids;171return $this;172}173174public function withDateCreatedBefore($date_created_before) {175$this->dateCreatedBefore = $date_created_before;176return $this;177}178179public function withDateCreatedAfter($date_created_after) {180$this->dateCreatedAfter = $date_created_after;181return $this;182}183184public function withDateModifiedBefore($date_modified_before) {185$this->dateModifiedBefore = $date_modified_before;186return $this;187}188189public function withDateModifiedAfter($date_modified_after) {190$this->dateModifiedAfter = $date_modified_after;191return $this;192}193194public function withClosedEpochBetween($min, $max) {195$this->closedEpochMin = $min;196$this->closedEpochMax = $max;197return $this;198}199200public function withCloserPHIDs(array $phids) {201$this->closerPHIDs = $phids;202return $this;203}204205public function needSubscriberPHIDs($bool) {206$this->needSubscriberPHIDs = $bool;207return $this;208}209210public function needProjectPHIDs($bool) {211$this->needProjectPHIDs = $bool;212return $this;213}214215public function withBridgedObjectPHIDs(array $phids) {216$this->bridgedObjectPHIDs = $phids;217return $this;218}219220public function withSubtypes(array $subtypes) {221$this->subtypes = $subtypes;222return $this;223}224225public function withColumnPHIDs(array $column_phids) {226$this->columnPHIDs = $column_phids;227return $this;228}229230public function withSpecificGroupByProjectPHID($project_phid) {231$this->specificGroupByProjectPHID = $project_phid;232return $this;233}234235public function newResultObject() {236return new ManiphestTask();237}238239protected function loadPage() {240$task_dao = new ManiphestTask();241$conn = $task_dao->establishConnection('r');242243$where = $this->buildWhereClause($conn);244245$group_column = qsprintf($conn, '');246switch ($this->groupBy) {247case self::GROUP_PROJECT:248$group_column = qsprintf(249$conn,250', projectGroupName.indexedObjectPHID projectGroupPHID');251break;252}253254$rows = queryfx_all(255$conn,256'%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',257$this->buildSelectClause($conn),258$group_column,259$task_dao->getTableName(),260$this->buildJoinClause($conn),261$where,262$this->buildGroupClause($conn),263$this->buildHavingClause($conn),264$this->buildOrderClause($conn),265$this->buildLimitClause($conn));266267switch ($this->groupBy) {268case self::GROUP_PROJECT:269$data = ipull($rows, null, 'id');270break;271default:272$data = $rows;273break;274}275276$data = $this->didLoadRawRows($data);277$tasks = $task_dao->loadAllFromArray($data);278279switch ($this->groupBy) {280case self::GROUP_PROJECT:281$results = array();282foreach ($rows as $row) {283$task = clone $tasks[$row['id']];284$task->attachGroupByProjectPHID($row['projectGroupPHID']);285$results[] = $task;286}287$tasks = $results;288break;289}290291return $tasks;292}293294protected function willFilterPage(array $tasks) {295if ($this->groupBy == self::GROUP_PROJECT) {296// We should only return project groups which the user can actually see.297$project_phids = mpull($tasks, 'getGroupByProjectPHID');298$projects = id(new PhabricatorProjectQuery())299->setViewer($this->getViewer())300->withPHIDs($project_phids)301->execute();302$projects = mpull($projects, null, 'getPHID');303304foreach ($tasks as $key => $task) {305if (!$task->getGroupByProjectPHID()) {306// This task is either not tagged with any projects, or only tagged307// with projects which we're ignoring because they're being queried308// for explicitly.309continue;310}311312if (empty($projects[$task->getGroupByProjectPHID()])) {313unset($tasks[$key]);314}315}316}317318return $tasks;319}320321protected function didFilterPage(array $tasks) {322$phids = mpull($tasks, 'getPHID');323324if ($this->needProjectPHIDs) {325$edge_query = id(new PhabricatorEdgeQuery())326->withSourcePHIDs($phids)327->withEdgeTypes(328array(329PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,330));331$edge_query->execute();332333foreach ($tasks as $task) {334$project_phids = $edge_query->getDestinationPHIDs(335array($task->getPHID()));336$task->attachProjectPHIDs($project_phids);337}338}339340if ($this->needSubscriberPHIDs) {341$subscriber_sets = id(new PhabricatorSubscribersQuery())342->withObjectPHIDs($phids)343->execute();344foreach ($tasks as $task) {345$subscribers = idx($subscriber_sets, $task->getPHID(), array());346$task->attachSubscriberPHIDs($subscribers);347}348}349350return $tasks;351}352353protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {354$where = parent::buildWhereClauseParts($conn);355356$where[] = $this->buildStatusWhereClause($conn);357$where[] = $this->buildOwnerWhereClause($conn);358359if ($this->taskIDs !== null) {360$where[] = qsprintf(361$conn,362'task.id in (%Ld)',363$this->taskIDs);364}365366if ($this->taskPHIDs !== null) {367$where[] = qsprintf(368$conn,369'task.phid in (%Ls)',370$this->taskPHIDs);371}372373if ($this->statuses !== null) {374$where[] = qsprintf(375$conn,376'task.status IN (%Ls)',377$this->statuses);378}379380if ($this->authorPHIDs !== null) {381$where[] = qsprintf(382$conn,383'task.authorPHID in (%Ls)',384$this->authorPHIDs);385}386387if ($this->dateCreatedAfter) {388$where[] = qsprintf(389$conn,390'task.dateCreated >= %d',391$this->dateCreatedAfter);392}393394if ($this->dateCreatedBefore) {395$where[] = qsprintf(396$conn,397'task.dateCreated <= %d',398$this->dateCreatedBefore);399}400401if ($this->dateModifiedAfter) {402$where[] = qsprintf(403$conn,404'task.dateModified >= %d',405$this->dateModifiedAfter);406}407408if ($this->dateModifiedBefore) {409$where[] = qsprintf(410$conn,411'task.dateModified <= %d',412$this->dateModifiedBefore);413}414415if ($this->closedEpochMin !== null) {416$where[] = qsprintf(417$conn,418'task.closedEpoch >= %d',419$this->closedEpochMin);420}421422if ($this->closedEpochMax !== null) {423$where[] = qsprintf(424$conn,425'task.closedEpoch <= %d',426$this->closedEpochMax);427}428429if ($this->closerPHIDs !== null) {430$where[] = qsprintf(431$conn,432'task.closerPHID IN (%Ls)',433$this->closerPHIDs);434}435436if ($this->priorities !== null) {437$where[] = qsprintf(438$conn,439'task.priority IN (%Ld)',440$this->priorities);441}442443if ($this->bridgedObjectPHIDs !== null) {444$where[] = qsprintf(445$conn,446'task.bridgedObjectPHID IN (%Ls)',447$this->bridgedObjectPHIDs);448}449450if ($this->subtypes !== null) {451$where[] = qsprintf(452$conn,453'task.subtype IN (%Ls)',454$this->subtypes);455}456457458if ($this->columnPHIDs !== null) {459$viewer = $this->getViewer();460461$columns = id(new PhabricatorProjectColumnQuery())462->setParentQuery($this)463->setViewer($viewer)464->withPHIDs($this->columnPHIDs)465->execute();466if (!$columns) {467throw new PhabricatorEmptyQueryException();468}469470// We must do board layout before we move forward because the column471// positions may not yet exist otherwise. An example is that newly472// created tasks may not yet be positioned in the backlog column.473474$projects = mpull($columns, 'getProject');475$projects = mpull($projects, null, 'getPHID');476477// The board layout engine needs to know about every object that it's478// going to be asked to do layout for. For now, we're just doing layout479// on every object on the boards. In the future, we could do layout on a480// smaller set of objects by using the constraints on this Query. For481// example, if the caller is only asking for open tasks, we only need482// to do layout on open tasks.483484// This fetches too many objects (every type of object tagged with the485// project, not just tasks). We could narrow it by querying the edge486// table on the Maniphest side, but there's currently no way to build487// that query with EdgeQuery.488$edge_query = id(new PhabricatorEdgeQuery())489->withSourcePHIDs(array_keys($projects))490->withEdgeTypes(491array(492PhabricatorProjectProjectHasObjectEdgeType::EDGECONST,493));494495$edge_query->execute();496$all_phids = $edge_query->getDestinationPHIDs();497498// Since we overfetched PHIDs, filter out any non-tasks we got back.499foreach ($all_phids as $key => $phid) {500if (phid_get_type($phid) !== ManiphestTaskPHIDType::TYPECONST) {501unset($all_phids[$key]);502}503}504505// If there are no tasks on the relevant boards, this query can't506// possibly hit anything so we're all done.507$task_phids = array_fuse($all_phids);508if (!$task_phids) {509throw new PhabricatorEmptyQueryException();510}511512// We know everything we need to know, so perform board layout.513$engine = id(new PhabricatorBoardLayoutEngine())514->setViewer($viewer)515->setFetchAllBoards(true)516->setBoardPHIDs(array_keys($projects))517->setObjectPHIDs($task_phids)518->executeLayout();519520// Find the tasks that are in the constraint columns after board layout521// completes.522$select_phids = array();523foreach ($columns as $column) {524$in_column = $engine->getColumnObjectPHIDs(525$column->getProjectPHID(),526$column->getPHID());527foreach ($in_column as $phid) {528$select_phids[$phid] = $phid;529}530}531532if (!$select_phids) {533throw new PhabricatorEmptyQueryException();534}535536$where[] = qsprintf(537$conn,538'task.phid IN (%Ls)',539$select_phids);540}541542if ($this->specificGroupByProjectPHID !== null) {543$where[] = qsprintf(544$conn,545'projectGroupName.indexedObjectPHID = %s',546$this->specificGroupByProjectPHID);547}548549return $where;550}551552private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {553static $map = array(554self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,555self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,556self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,557self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,558self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,559);560561switch ($this->status) {562case self::STATUS_ANY:563return null;564case self::STATUS_OPEN:565return qsprintf(566$conn,567'task.status IN (%Ls)',568ManiphestTaskStatus::getOpenStatusConstants());569case self::STATUS_CLOSED:570return qsprintf(571$conn,572'task.status IN (%Ls)',573ManiphestTaskStatus::getClosedStatusConstants());574default:575$constant = idx($map, $this->status);576if (!$constant) {577throw new Exception(pht("Unknown status query '%s'!", $this->status));578}579return qsprintf(580$conn,581'task.status = %s',582$constant);583}584}585586private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {587$subclause = array();588589if ($this->noOwner) {590$subclause[] = qsprintf(591$conn,592'task.ownerPHID IS NULL');593}594595if ($this->anyOwner) {596$subclause[] = qsprintf(597$conn,598'task.ownerPHID IS NOT NULL');599}600601if ($this->ownerPHIDs !== null) {602$subclause[] = qsprintf(603$conn,604'task.ownerPHID IN (%Ls)',605$this->ownerPHIDs);606}607608if (!$subclause) {609return qsprintf($conn, '');610}611612return qsprintf($conn, '%LO', $subclause);613}614615protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {616$open_statuses = ManiphestTaskStatus::getOpenStatusConstants();617$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;618$task_table = $this->newResultObject()->getTableName();619620$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;621$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;622623$joins = array();624if ($this->hasOpenParents !== null) {625if ($this->hasOpenParents) {626$join_type = qsprintf($conn, 'JOIN');627} else {628$join_type = qsprintf($conn, 'LEFT JOIN');629}630631$joins[] = qsprintf(632$conn,633'%Q %T e_parent634ON e_parent.src = task.phid635AND e_parent.type = %d636%Q %T parent637ON e_parent.dst = parent.phid638AND parent.status IN (%Ls)',639$join_type,640$edge_table,641$parent_type,642$join_type,643$task_table,644$open_statuses);645}646647if ($this->hasOpenSubtasks !== null) {648if ($this->hasOpenSubtasks) {649$join_type = qsprintf($conn, 'JOIN');650} else {651$join_type = qsprintf($conn, 'LEFT JOIN');652}653654$joins[] = qsprintf(655$conn,656'%Q %T e_subtask657ON e_subtask.src = task.phid658AND e_subtask.type = %d659%Q %T subtask660ON e_subtask.dst = subtask.phid661AND subtask.status IN (%Ls)',662$join_type,663$edge_table,664$subtask_type,665$join_type,666$task_table,667$open_statuses);668}669670if ($this->subscriberPHIDs !== null) {671$joins[] = qsprintf(672$conn,673'JOIN %T e_ccs ON e_ccs.src = task.phid '.674'AND e_ccs.type = %s '.675'AND e_ccs.dst in (%Ls)',676PhabricatorEdgeConfig::TABLE_NAME_EDGE,677PhabricatorObjectHasSubscriberEdgeType::EDGECONST,678$this->subscriberPHIDs);679}680681switch ($this->groupBy) {682case self::GROUP_PROJECT:683$ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();684if ($ignore_group_phids) {685$joins[] = qsprintf(686$conn,687'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src688AND projectGroup.type = %d689AND projectGroup.dst NOT IN (%Ls)',690$edge_table,691PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,692$ignore_group_phids);693} else {694$joins[] = qsprintf(695$conn,696'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src697AND projectGroup.type = %d',698$edge_table,699PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);700}701$joins[] = qsprintf(702$conn,703'LEFT JOIN %T projectGroupName704ON projectGroup.dst = projectGroupName.indexedObjectPHID',705id(new ManiphestNameIndex())->getTableName());706break;707}708709if ($this->parentTaskIDs !== null) {710$joins[] = qsprintf(711$conn,712'JOIN %T e_has_parent713ON e_has_parent.src = task.phid714AND e_has_parent.type = %d715JOIN %T has_parent716ON e_has_parent.dst = has_parent.phid717AND has_parent.id IN (%Ld)',718$edge_table,719$parent_type,720$task_table,721$this->parentTaskIDs);722}723724if ($this->subtaskIDs !== null) {725$joins[] = qsprintf(726$conn,727'JOIN %T e_has_subtask728ON e_has_subtask.src = task.phid729AND e_has_subtask.type = %d730JOIN %T has_subtask731ON e_has_subtask.dst = has_subtask.phid732AND has_subtask.id IN (%Ld)',733$edge_table,734$subtask_type,735$task_table,736$this->subtaskIDs);737}738739$joins[] = parent::buildJoinClauseParts($conn);740741return $joins;742}743744protected function buildGroupClause(AphrontDatabaseConnection $conn) {745$joined_multiple_rows =746($this->hasOpenParents !== null) ||747($this->hasOpenSubtasks !== null) ||748($this->parentTaskIDs !== null) ||749($this->subtaskIDs !== null) ||750$this->shouldGroupQueryResultRows();751752$joined_project_name = ($this->groupBy == self::GROUP_PROJECT);753754// If we're joining multiple rows, we need to group the results by the755// task IDs.756if ($joined_multiple_rows) {757if ($joined_project_name) {758return qsprintf($conn, 'GROUP BY task.phid, projectGroup.dst');759} else {760return qsprintf($conn, 'GROUP BY task.phid');761}762}763764return qsprintf($conn, '');765}766767768protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {769$having = parent::buildHavingClauseParts($conn);770771if ($this->hasOpenParents !== null) {772if (!$this->hasOpenParents) {773$having[] = qsprintf(774$conn,775'COUNT(parent.phid) = 0');776}777}778779if ($this->hasOpenSubtasks !== null) {780if (!$this->hasOpenSubtasks) {781$having[] = qsprintf(782$conn,783'COUNT(subtask.phid) = 0');784}785}786787return $having;788}789790791/**792* Return project PHIDs which we should ignore when grouping tasks by793* project. For example, if a user issues a query like:794*795* Tasks tagged with all projects: Frontend, Bugs796*797* ...then we don't show "Frontend" or "Bugs" groups in the result set, since798* they're meaningless as all results are in both groups.799*800* Similarly, for queries like:801*802* Tasks tagged with any projects: Public Relations803*804* ...we ignore the single project, as every result is in that project. (In805* the case that there are several "any" projects, we do not ignore them.)806*807* @return list<phid> Project PHIDs which should be ignored in query808* construction.809*/810private function getIgnoreGroupedProjectPHIDs() {811// Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't812// impact the results, but we might end up with a better query plan.813// Investigate this on real data? This is likely very rare.814815$edge_types = array(816PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,817);818819$phids = array();820821$phids[] = $this->getEdgeLogicValues(822$edge_types,823array(824PhabricatorQueryConstraint::OPERATOR_AND,825));826827$any = $this->getEdgeLogicValues(828$edge_types,829array(830PhabricatorQueryConstraint::OPERATOR_OR,831));832if (count($any) == 1) {833$phids[] = $any;834}835836return array_mergev($phids);837}838839public function getBuiltinOrders() {840$orders = array(841'priority' => array(842'vector' => array('priority', 'id'),843'name' => pht('Priority'),844'aliases' => array(self::ORDER_PRIORITY),845),846'updated' => array(847'vector' => array('updated', 'id'),848'name' => pht('Date Updated (Latest First)'),849'aliases' => array(self::ORDER_MODIFIED),850),851'outdated' => array(852'vector' => array('-updated', '-id'),853'name' => pht('Date Updated (Oldest First)'),854),855'closed' => array(856'vector' => array('closed', 'id'),857'name' => pht('Date Closed (Latest First)'),858),859'title' => array(860'vector' => array('title', 'id'),861'name' => pht('Title'),862'aliases' => array(self::ORDER_TITLE),863),864) + parent::getBuiltinOrders();865866// Alias the "newest" builtin to the historical key for it.867$orders['newest']['aliases'][] = self::ORDER_CREATED;868869$orders = array_select_keys(870$orders,871array(872'priority',873'updated',874'outdated',875'newest',876'oldest',877'closed',878'title',879)) + $orders;880881return $orders;882}883884public function getOrderableColumns() {885return parent::getOrderableColumns() + array(886'priority' => array(887'table' => 'task',888'column' => 'priority',889'type' => 'int',890),891'owner' => array(892'table' => 'task',893'column' => 'ownerOrdering',894'null' => 'head',895'reverse' => true,896'type' => 'string',897),898'status' => array(899'table' => 'task',900'column' => 'status',901'type' => 'string',902'reverse' => true,903),904'project' => array(905'table' => 'projectGroupName',906'column' => 'indexedObjectName',907'type' => 'string',908'null' => 'head',909'reverse' => true,910),911'title' => array(912'table' => 'task',913'column' => 'title',914'type' => 'string',915'reverse' => true,916),917'updated' => array(918'table' => 'task',919'column' => 'dateModified',920'type' => 'int',921),922'closed' => array(923'table' => 'task',924'column' => 'closedEpoch',925'type' => 'int',926'null' => 'tail',927),928);929}930931protected function newPagingMapFromCursorObject(932PhabricatorQueryCursor $cursor,933array $keys) {934935$task = $cursor->getObject();936937$map = array(938'id' => (int)$task->getID(),939'priority' => (int)$task->getPriority(),940'owner' => $task->getOwnerOrdering(),941'status' => $task->getStatus(),942'title' => $task->getTitle(),943'updated' => (int)$task->getDateModified(),944'closed' => $task->getClosedEpoch(),945);946947if (isset($keys['project'])) {948$value = null;949950$group_phid = $task->getGroupByProjectPHID();951if ($group_phid) {952$paging_projects = id(new PhabricatorProjectQuery())953->setViewer($this->getViewer())954->withPHIDs(array($group_phid))955->execute();956if ($paging_projects) {957$value = head($paging_projects)->getName();958}959}960961$map['project'] = $value;962}963964foreach ($keys as $key) {965if ($this->isCustomFieldOrderKey($key)) {966$map += $this->getPagingValueMapForCustomFields($task);967break;968}969}970971return $map;972}973974protected function newExternalCursorStringForResult($object) {975$id = $object->getID();976977if ($this->groupBy == self::GROUP_PROJECT) {978return rtrim($id.'.'.$object->getGroupByProjectPHID(), '.');979}980981return $id;982}983984protected function newInternalCursorFromExternalCursor($cursor) {985list($task_id, $group_phid) = $this->parseCursor($cursor);986987$cursor_object = parent::newInternalCursorFromExternalCursor($cursor);988989if ($group_phid !== null) {990$project = id(new PhabricatorProjectQuery())991->setViewer($this->getViewer())992->withPHIDs(array($group_phid))993->execute();994995if (!$project) {996$this->throwCursorException(997pht(998'Group PHID ("%s") component of cursor ("%s") is not valid.',999$group_phid,1000$cursor));1001}10021003$cursor_object->getObject()->attachGroupByProjectPHID($group_phid);1004}10051006return $cursor_object;1007}10081009protected function applyExternalCursorConstraintsToQuery(1010PhabricatorCursorPagedPolicyAwareQuery $subquery,1011$cursor) {1012list($task_id, $group_phid) = $this->parseCursor($cursor);10131014$subquery->withIDs(array($task_id));10151016if ($group_phid) {1017$subquery->setGroupBy(self::GROUP_PROJECT);10181019// The subquery needs to return exactly one result. If a task is in1020// several projects, the query may naturally return several results.1021// Specify that we want only the particular instance of the task in1022// the specified project.1023$subquery->withSpecificGroupByProjectPHID($group_phid);1024}1025}102610271028private function parseCursor($cursor) {1029// Split a "123.PHID-PROJ-abcd" cursor into a "Task ID" part and a1030// "Project PHID" part.10311032$parts = explode('.', $cursor, 2);10331034if (count($parts) < 2) {1035$parts[] = null;1036}10371038if (!strlen($parts[1])) {1039$parts[1] = null;1040}10411042return $parts;1043}10441045protected function getPrimaryTableAlias() {1046return 'task';1047}10481049public function getQueryApplicationClass() {1050return 'PhabricatorManiphestApplication';1051}10521053}105410551056