Page MenuHomec4science

PhabricatorProjectQuery.php
No OneTemporary

File Metadata

Created
Wed, Nov 20, 12:36

PhabricatorProjectQuery.php

<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $slugs;
private $slugNormals;
private $slugMap;
private $allSlugs;
private $names;
private $nameTokens;
private $icons;
private $colors;
private $ancestorPHIDs;
private $parentPHIDs;
private $isMilestone;
private $hasSubprojects;
private $minDepth;
private $maxDepth;
private $minMilestoneNumber;
private $maxMilestoneNumber;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $statuses;
private $needSlugs;
private $needMembers;
private $needAncestorMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function withParentProjectPHIDs($parent_phids) {
$this->parentPHIDs = $parent_phids;
return $this;
}
public function withAncestorProjectPHIDs($ancestor_phids) {
$this->ancestorPHIDs = $ancestor_phids;
return $this;
}
public function withIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function withHasSubprojects($has_subprojects) {
$this->hasSubprojects = $has_subprojects;
return $this;
}
public function withDepthBetween($min, $max) {
$this->minDepth = $min;
$this->maxDepth = $max;
return $this;
}
public function withMilestoneNumberBetween($min, $max) {
$this->minMilestoneNumber = $min;
$this->maxMilestoneNumber = $max;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needAncestorMembers($need_ancestor_members) {
$this->needAncestorMembers = $need_ancestor_members;
return $this;
}
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$project = $this->loadCursorObject($cursor);
return array(
'name' => $project->getName(),
);
}
public function getSlugMap() {
if ($this->slugMap === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->slugMap;
}
protected function willExecute() {
$this->slugMap = array();
$this->slugNormals = array();
$this->allSlugs = array();
if ($this->slugs) {
foreach ($this->slugs as $slug) {
if (PhabricatorSlug::isValidProjectSlug($slug)) {
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
$this->slugNormals[$slug] = $normal;
$this->allSlugs[$normal] = $normal;
}
// NOTE: At least for now, we query for the normalized slugs but also
// for the slugs exactly as entered. This allows older projects with
// slugs that are no longer valid to continue to work.
$this->allSlugs[$slug] = $slug;
}
}
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $projects) {
$ancestor_paths = array();
foreach ($projects as $project) {
foreach ($project->getAncestorProjectPaths() as $path) {
$ancestor_paths[$path] = $path;
}
}
if ($ancestor_paths) {
$ancestors = id(new PhabricatorProject())->loadAllWhere(
'projectPath IN (%Ls)',
$ancestor_paths);
} else {
$ancestors = array();
}
$projects = $this->linkProjectGraph($projects, $ancestors);
$viewer_phid = $this->getViewer()->getPHID();
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$types = array();
$types[] = $material_type;
if ($this->needWatchers) {
$types[] = $watcher_type;
}
$all_graph = $this->getAllReachableAncestors($projects);
if ($this->needAncestorMembers) {
$src_projects = $all_graph;
} else {
$src_projects = $projects;
}
$all_sources = array();
foreach ($src_projects as $project) {
if ($project->isMilestone()) {
$phid = $project->getParentProjectPHID();
} else {
$phid = $project->getPHID();
}
$all_sources[$phid] = $phid;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_sources)
->withEdgeTypes($types);
$need_all_edges =
$this->needMembers ||
$this->needWatchers ||
$this->needAncestorMembers;
// If we only need to know if the viewer is a member, we can restrict
// the query to just their PHID.
$any_edges = true;
if (!$need_all_edges) {
if ($viewer_phid) {
$edge_query->withDestinationPHIDs(array($viewer_phid));
} else {
// If we don't need members or watchers and don't have a viewer PHID
// (viewer is logged-out or omnipotent), they'll never be a member
// so we don't need to issue this query at all.
$any_edges = false;
}
}
if ($any_edges) {
$edge_query->execute();
}
$membership_projects = array();
foreach ($src_projects as $project) {
$project_phid = $project->getPHID();
if ($project->isMilestone()) {
$source_phids = array($project->getParentProjectPHID());
} else {
$source_phids = array($project_phid);
}
if ($any_edges) {
$member_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($material_type));
} else {
$member_phids = array();
}
if (in_array($viewer_phid, $member_phids)) {
$membership_projects[$project_phid] = $project;
}
if ($this->needMembers || $this->needAncestorMembers) {
$project->attachMemberPHIDs($member_phids);
}
if ($this->needWatchers) {
$watcher_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($watcher_type));
$project->attachWatcherPHIDs($watcher_phids);
$project->setIsUserWatcher(
$viewer_phid,
in_array($viewer_phid, $watcher_phids));
}
}
// If we loaded ancestor members, we've already populated membership
// lists above, so we can skip this step.
if (!$this->needAncestorMembers) {
$member_graph = $this->getAllReachableAncestors($membership_projects);
foreach ($all_graph as $phid => $project) {
$is_member = isset($member_graph[$phid]);
$project->setIsUserMember($viewer_phid, $is_member);
}
}
return $projects;
}
protected function didFilterPage(array $projects) {
if ($this->needImages) {
$default = null;
$file_phids = mpull($projects, 'getProfileImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($projects as $project) {
$file = idx($files, $project->getProfileImagePHID());
if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'project.png');
}
$file = $default;
}
$project->attachProfileImageFile($file);
}
}
$this->loadSlugs($projects);
return $projects;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
pht(
"Unknown project status '%s'!",
$this->status));
}
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$filter);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'slug.slug IN (%Ls)',
$this->allSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn,
'color IN (%Ls)',
$this->colors);
}
if ($this->parentPHIDs !== null) {
$where[] = qsprintf(
$conn,
'parentProjectPHID IN (%Ls)',
$this->parentPHIDs);
}
if ($this->ancestorPHIDs !== null) {
$ancestor_paths = queryfx_all(
$conn,
'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
id(new PhabricatorProject())->getTableName(),
$this->ancestorPHIDs);
if (!$ancestor_paths) {
throw new PhabricatorEmptyQueryException();
}
$sql = array();
foreach ($ancestor_paths as $ancestor_path) {
$sql[] = qsprintf(
$conn,
'(projectPath LIKE %> AND projectDepth > %d)',
$ancestor_path['projectPath'],
$ancestor_path['projectDepth']);
}
$where[] = '('.implode(' OR ', $sql).')';
$where[] = qsprintf(
$conn,
'parentProjectPHID IS NOT NULL');
}
if ($this->isMilestone !== null) {
if ($this->isMilestone) {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NULL');
}
}
if ($this->hasSubprojects !== null) {
$where[] = qsprintf(
$conn,
'hasSubprojects = %d',
(int)$this->hasSubprojects);
}
if ($this->minDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth >= %d',
$this->minDepth);
}
if ($this->maxDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth <= %d',
$this->maxDepth);
}
if ($this->minMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber >= %d',
$this->minMilestoneNumber);
}
if ($this->maxMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber <= %d',
$this->maxMilestoneNumber);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->memberPHIDs || $this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
foreach ($this->nameTokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function linkProjectGraph(array $projects, array $ancestors) {
$ancestor_map = mpull($ancestors, null, 'getPHID');
$projects_map = mpull($projects, null, 'getPHID');
$all_map = $projects_map + $ancestor_map;
$done = array();
foreach ($projects as $key => $project) {
$seen = array($project->getPHID() => true);
if (!$this->linkProject($project, $all_map, $done, $seen)) {
$this->didRejectResult($project);
unset($projects[$key]);
continue;
}
foreach ($project->getAncestorProjects() as $ancestor) {
$seen[$ancestor->getPHID()] = true;
}
}
return $projects;
}
private function linkProject($project, array $all, array $done, array $seen) {
$parent_phid = $project->getParentProjectPHID();
// This project has no parent, so just attach `null` and return.
if (!$parent_phid) {
$project->attachParentProject(null);
return true;
}
// This project has a parent, but it failed to load.
if (empty($all[$parent_phid])) {
return false;
}
// Test for graph cycles. If we encounter one, we're going to hide the
// entire cycle since we can't meaningfully resolve it.
if (isset($seen[$parent_phid])) {
return false;
}
$seen[$parent_phid] = true;
$parent = $all[$parent_phid];
$project->attachParentProject($parent);
if (!empty($done[$parent_phid])) {
return true;
}
return $this->linkProject($parent, $all, $done, $seen);
}
private function getAllReachableAncestors(array $projects) {
$ancestors = array();
$seen = mpull($projects, null, 'getPHID');
$stack = $projects;
while ($stack) {
$project = array_pop($stack);
$phid = $project->getPHID();
$ancestors[$phid] = $project;
$parent_phid = $project->getParentProjectPHID();
if (!$parent_phid) {
continue;
}
if (isset($seen[$parent_phid])) {
continue;
}
$seen[$parent_phid] = true;
$stack[] = $project->getParentProject();
}
return $ancestors;
}
private function loadSlugs(array $projects) {
// Build a map from primary slugs to projects.
$primary_map = array();
foreach ($projects as $project) {
$primary_slug = $project->getPrimarySlug();
if ($primary_slug === null) {
continue;
}
$primary_map[$primary_slug] = $project;
}
// Link up all of the queried slugs which correspond to primary
// slugs. If we can link up everything from this (no slugs were queried,
// or only primary slugs were queried) we don't need to load anything
// else.
$unknown = $this->slugNormals;
foreach ($unknown as $input => $normal) {
if (isset($primary_map[$input])) {
$match = $input;
} else if (isset($primary_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $primary_map[$match]->getPHID(),
);
unset($unknown[$input]);
}
// If we need slugs, we have to load everything.
// If we still have some queried slugs which we haven't mapped, we only
// need to look for them.
// If we've mapped everything, we don't have to do any work.
$project_phids = mpull($projects, 'getPHID');
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls)',
$project_phids);
} else if ($unknown) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls) AND slug IN (%Ls)',
$project_phids,
$unknown);
} else {
$slugs = array();
}
// Link up any slugs we were not able to link up earlier.
$extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
foreach ($unknown as $input => $normal) {
if (isset($extra_map[$input])) {
$match = $input;
} else if (isset($extra_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $extra_map[$match],
);
unset($unknown[$input]);
}
if ($this->needSlugs) {
$slug_groups = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slug_groups, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
}
}

Event Timeline