@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
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}