@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 283 lines 8.0 kB view raw
1<?php 2 3final class PhabricatorFeedTransactionQuery 4 extends PhabricatorCursorPagedPolicyAwareQuery { 5 6 private $phids; 7 private $authorPHIDs; 8 private $objectTypes; 9 private $objectPHIDs; 10 private $createdMin; 11 private $createdMax; 12 13 public function withPHIDs(array $phids) { 14 $this->phids = $phids; 15 return $this; 16 } 17 18 public function withAuthorPHIDs(array $phids) { 19 $this->authorPHIDs = $phids; 20 return $this; 21 } 22 23 public function withObjectTypes(array $types) { 24 $this->objectTypes = $types; 25 return $this; 26 } 27 28 public function withObjectPHIDs(array $object_phids) { 29 $this->objectPHIDs = $object_phids; 30 return $this; 31 } 32 33 public function withDateCreatedBetween($min, $max) { 34 $this->createdMin = $min; 35 $this->createdMax = $max; 36 return $this; 37 } 38 39 public function newResultObject() { 40 // Return an arbitrary valid transaction object. The actual query may 41 // return objects of any subclass of "ApplicationTransaction" when it is 42 // executed, but we need to pick something concrete here to make some 43 // integrations work (like automatic handling of PHIDs in data export). 44 return new PhabricatorUserTransaction(); 45 } 46 47 protected function loadPage() { 48 $this->normalizeObjectPHIDs(); 49 $queries = $this->newTransactionQueries(); 50 51 $xactions = array(); 52 53 if ($this->shouldLimitResults()) { 54 $limit = $this->getRawResultLimit(); 55 if (!$limit) { 56 $limit = null; 57 } 58 } else { 59 $limit = null; 60 } 61 62 // We're doing a bit of manual work to get paging working, because this 63 // query aggregates the results of a large number of subqueries. 64 65 // Overall, we're ordering transactions by "<dateCreated, phid>". Ordering 66 // by PHID is not very meaningful, but we don't need the ordering to be 67 // especially meaningful, just consistent. Using PHIDs is easy and does 68 // everything we need it to technically. 69 70 // To actually configure paging, if we have an external cursor, we load 71 // the internal cursor first. Then we pass it to each subquery and the 72 // subqueries pretend they just loaded a page where it was the last object. 73 // This configures their queries properly and we can aggregate a cohesive 74 // set of results by combining all the queries. 75 76 $cursor = $this->getExternalCursorString(); 77 if ($cursor !== null) { 78 $cursor_object = $this->newInternalCursorFromExternalCursor($cursor); 79 } else { 80 $cursor_object = null; 81 } 82 83 $is_reversed = $this->getIsQueryOrderReversed(); 84 85 $created_min = $this->createdMin; 86 $created_max = $this->createdMax; 87 88 $xaction_phids = $this->phids; 89 $author_phids = $this->authorPHIDs; 90 $object_phids = $this->objectPHIDs; 91 92 foreach ($queries as $query) { 93 $query->withDateCreatedBetween($created_min, $created_max); 94 95 if ($xaction_phids !== null) { 96 $query->withPHIDs($xaction_phids); 97 } 98 99 if ($author_phids !== null) { 100 $query->withAuthorPHIDs($author_phids); 101 } 102 103 if ($object_phids !== null) { 104 $query->withObjectPHIDs($object_phids); 105 } 106 107 if ($limit !== null) { 108 $query->setLimit($limit); 109 } 110 111 if ($cursor_object !== null) { 112 $query 113 ->setAggregatePagingCursor($cursor_object) 114 ->setIsQueryOrderReversed($is_reversed); 115 } 116 117 $query->setOrder('global'); 118 119 $query_xactions = $query->execute(); 120 foreach ($query_xactions as $query_xaction) { 121 $xactions[] = $query_xaction; 122 } 123 124 $xactions = msortv($xactions, 'newGlobalSortVector'); 125 if ($is_reversed) { 126 $xactions = array_reverse($xactions); 127 } 128 129 if ($limit !== null) { 130 $xactions = array_slice($xactions, 0, $limit); 131 132 // If we've found enough transactions to fill up the entire requested 133 // page size, we can narrow the search window: transactions after the 134 // last transaction we've found so far can't possibly be part of the 135 // result set. 136 137 if (count($xactions) === $limit) { 138 $last_date = last($xactions)->getDateCreated(); 139 if ($is_reversed) { 140 if ($created_max === null) { 141 $created_max = $last_date; 142 } else { 143 $created_max = min($created_max, $last_date); 144 } 145 } else { 146 if ($created_min === null) { 147 $created_min = $last_date; 148 } else { 149 $created_min = max($created_min, $last_date); 150 } 151 } 152 } 153 } 154 } 155 156 return $xactions; 157 } 158 159 public function getQueryApplicationClass() { 160 return PhabricatorFeedApplication::class; 161 } 162 163 private function newTransactionQueries() { 164 $viewer = $this->getViewer(); 165 166 $queries = id(new PhutilClassMapQuery()) 167 ->setAncestorClass(PhabricatorApplicationTransactionQuery::class) 168 ->execute(); 169 170 // Remove TransactionQuery classes of disabled apps. Increases query 171 // performance and decreases likeliness of a "Query Overheated" error if 172 // an app got disabled so data in it cannot be accessed anymore anyway. 173 // See https://secure.phabricator.com/T13133, https://we.phorge.it/T15642 174 foreach ($queries as $key => $query) { 175 $app = $query->getQueryApplicationClass(); 176 if ($app !== null && 177 !PhabricatorApplication::isClassInstalledForViewer($app, $viewer)) { 178 unset($queries[$key]); 179 } 180 } 181 182 $type_map = array(); 183 184 // If we're querying for specific transaction PHIDs, we only need to 185 // consider queries which may load transactions with subtypes present 186 // in the list. 187 188 // For example, if we're loading Maniphest Task transaction PHIDs, we know 189 // we only have to look at Maniphest Task transactions, since other types 190 // of objects will never have the right transaction PHIDs. 191 192 $xaction_phids = $this->phids; 193 if ($xaction_phids) { 194 foreach ($xaction_phids as $xaction_phid) { 195 $type_map[phid_get_subtype($xaction_phid)] = true; 196 } 197 } 198 199 $object_types = $this->objectTypes; 200 if ($object_types) { 201 $object_types = array_fuse($object_types); 202 } 203 204 $results = array(); 205 foreach ($queries as $query) { 206 $query_type = $query->getTemplateApplicationTransaction() 207 ->getApplicationTransactionType(); 208 209 if ($type_map) { 210 if (!isset($type_map[$query_type])) { 211 continue; 212 } 213 } 214 215 if ($object_types) { 216 if (!isset($object_types[$query_type])) { 217 continue; 218 } 219 } 220 221 $results[] = id(clone $query) 222 ->setViewer($viewer) 223 ->setParentQuery($this); 224 } 225 226 return $results; 227 } 228 229 protected function newExternalCursorStringForResult($object) { 230 return (string)$object->getPHID(); 231 } 232 233 protected function applyExternalCursorConstraintsToQuery( 234 PhabricatorCursorPagedPolicyAwareQuery $subquery, 235 $cursor) { 236 237 $subquery->withPHIDs(array($cursor)); 238 } 239 240 private function normalizeObjectPHIDs() { 241 if (!$this->objectPHIDs) { 242 return; 243 } 244 245 $have_non_phids = false; 246 foreach ($this->objectPHIDs as $name) { 247 if (strncmp($name, 'PHID-', 5)) { 248 $have_non_phids = true; 249 break; 250 } 251 } 252 253 if ($have_non_phids) { 254 255 // "Names" field in ObjectQuery also handles PHIDs. 256 $objects = id(new PhabricatorObjectQuery()) 257 ->setViewer($this->getViewer()) 258 ->withNames($this->objectPHIDs) 259 ->execute(); 260 261 if (!$objects) { 262 // nothing resolved to anything. 263 throw new PhabricatorSearchConstraintException( 264 pht("objectPHID inputs didn't match any known objects.")); 265 } 266 267 $phids = mpull($objects, 'getPHID'); 268 269 } else { 270 $phids = $this->objectPHIDs; 271 } 272 273 $phid_types = array(); 274 foreach ($phids as $phid) { 275 $phid_type = phid_get_type($phid); 276 $phid_types[$phid_type] = $phid_type; 277 } 278 279 $this->objectPHIDs = $phids; 280 $this->objectTypes = $phid_types; 281 } 282 283}