@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
3/**
4 * Load object edges created by @{class:PhabricatorEdgeEditor}.
5 *
6 * name=Querying Edges
7 * $src = $earth_phid;
8 * $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
9 *
10 * // Load the earth's satellites.
11 * $satellite_edges = id(new PhabricatorEdgeQuery())
12 * ->withSourcePHIDs(array($src))
13 * ->withEdgeTypes(array($type))
14 * ->execute();
15 *
16 * For more information on edges, see @{article:Using Edges}.
17 *
18 * @task config Configuring the Query
19 * @task exec Executing the Query
20 * @task internal Internal
21 */
22final class PhabricatorEdgeQuery extends PhabricatorQuery {
23
24 private $sourcePHIDs;
25 private $destPHIDs;
26 private $edgeTypes;
27 private $resultSet;
28
29 const ORDER_OLDEST_FIRST = 'order:oldest';
30 const ORDER_NEWEST_FIRST = 'order:newest';
31 private $order = self::ORDER_NEWEST_FIRST;
32
33 private $needEdgeData;
34
35
36/* -( Configuring the Query )---------------------------------------------- */
37
38
39 /**
40 * Find edges originating at one or more source PHIDs. You MUST provide this
41 * to execute an edge query.
42 *
43 * @param list $source_phids List of source PHIDs.
44 * @return $this
45 *
46 * @task config
47 */
48 public function withSourcePHIDs(array $source_phids) {
49 if (!$source_phids) {
50 throw new Exception(
51 pht(
52 'Edge list passed to "withSourcePHIDs(...)" is empty, but it must '.
53 'be nonempty.'));
54 }
55
56 $this->sourcePHIDs = $source_phids;
57 return $this;
58 }
59
60
61 /**
62 * Find edges terminating at one or more destination PHIDs.
63 *
64 * @param list $dest_phids List of destination PHIDs.
65 * @return $this
66 *
67 */
68 public function withDestinationPHIDs(array $dest_phids) {
69 $this->destPHIDs = $dest_phids;
70 return $this;
71 }
72
73
74 /**
75 * Find edges of specific types.
76 *
77 * @param list $types List of PhabricatorEdgeConfig type constants.
78 * @return $this
79 *
80 * @task config
81 */
82 public function withEdgeTypes(array $types) {
83 $this->edgeTypes = $types;
84 return $this;
85 }
86
87
88 /**
89 * Configure the order edge results are returned in.
90 *
91 * @param string $order Order constant.
92 * @return $this
93 *
94 * @task config
95 */
96 public function setOrder($order) {
97 $this->order = $order;
98 return $this;
99 }
100
101
102 /**
103 * When loading edges, also load edge data.
104 *
105 * @param bool $need True to load edge data.
106 * @return $this
107 *
108 * @task config
109 */
110 public function needEdgeData($need) {
111 $this->needEdgeData = $need;
112 return $this;
113 }
114
115
116/* -( Executing the Query )------------------------------------------------ */
117
118
119 /**
120 * Convenience method for loading destination PHIDs with one source and one
121 * edge type. Equivalent to building a full query, but simplifies a common
122 * use case.
123 *
124 * @param string $src_phid Source PHID.
125 * @param int $edge_type Edge type constant
126 * (SomeClassEdgeType::EDGECONST).
127 * @return list<string> List of destination PHIDs.
128 */
129 public static function loadDestinationPHIDs($src_phid, $edge_type) {
130 $edges = id(new PhabricatorEdgeQuery())
131 ->withSourcePHIDs(array($src_phid))
132 ->withEdgeTypes(array($edge_type))
133 ->execute();
134 return array_keys($edges[$src_phid][$edge_type]);
135 }
136
137 /**
138 * Convenience method for loading a single edge's metadata for
139 * a given source, destination, and edge type. Returns null
140 * if the edge does not exist or does not have metadata. Builds
141 * and immediately executes a full query.
142 *
143 * @param string $src_phid Source PHID.
144 * @param string $edge_type Edge type constant.
145 * @param string $dest_phid Destination PHID.
146 * @return mixed Edge annotation, or null.
147 */
148 public static function loadSingleEdgeData($src_phid, $edge_type, $dest_phid) {
149 $edges = id(new PhabricatorEdgeQuery())
150 ->withSourcePHIDs(array($src_phid))
151 ->withEdgeTypes(array($edge_type))
152 ->withDestinationPHIDs(array($dest_phid))
153 ->needEdgeData(true)
154 ->execute();
155
156 if (isset($edges[$src_phid][$edge_type][$dest_phid]['data'])) {
157 return $edges[$src_phid][$edge_type][$dest_phid]['data'];
158 }
159 return null;
160 }
161
162
163 /**
164 * Load specified edges.
165 *
166 * @task exec
167 */
168 public function execute() {
169 if ($this->sourcePHIDs === null) {
170 throw new Exception(
171 pht(
172 'You must use "withSourcePHIDs()" to query edges.'));
173 }
174
175 $sources = phid_group_by_type($this->sourcePHIDs);
176
177 $result = array();
178
179 // When a query specifies types, make sure we return data for all queried
180 // types.
181 if ($this->edgeTypes) {
182 foreach ($this->sourcePHIDs as $phid) {
183 foreach ($this->edgeTypes as $type) {
184 $result[$phid][$type] = array();
185 }
186 }
187 }
188
189 foreach ($sources as $type => $phids) {
190 $conn_r = PhabricatorEdgeConfig::establishConnection($type, 'r');
191
192 $where = $this->buildWhereClause($conn_r);
193 $order = $this->buildOrderClause($conn_r);
194
195 $edges = queryfx_all(
196 $conn_r,
197 'SELECT edge.* FROM %T edge %Q %Q',
198 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
199 $where,
200 $order);
201
202 if ($this->needEdgeData) {
203 $data_ids = array_filter(ipull($edges, 'dataID'));
204 $data_map = array();
205 if ($data_ids) {
206 $data_rows = queryfx_all(
207 $conn_r,
208 'SELECT edgedata.* FROM %T edgedata WHERE id IN (%Ld)',
209 PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
210 $data_ids);
211 foreach ($data_rows as $row) {
212 $data_map[$row['id']] = idx(
213 phutil_json_decode($row['data']),
214 'data');
215 }
216 }
217 foreach ($edges as $key => $edge) {
218 $edges[$key]['data'] = idx($data_map, $edge['dataID'], array());
219 }
220 }
221
222 foreach ($edges as $edge) {
223 $result[$edge['src']][$edge['type']][$edge['dst']] = $edge;
224 }
225 }
226
227 $this->resultSet = $result;
228 return $result;
229 }
230
231
232 /**
233 * Convenience function for selecting edge destination PHIDs after calling
234 * execute().
235 *
236 * Returns a flat list of PHIDs matching the provided source PHID and type
237 * filters. By default, the filters are empty so all PHIDs will be returned.
238 * For example, if you're doing a batch query from several sources, you might
239 * write code like this:
240 *
241 * $query = new PhabricatorEdgeQuery();
242 * $query->setViewer($viewer);
243 * $query->withSourcePHIDs(mpull($objects, 'getPHID'));
244 * $query->withEdgeTypes(array($some_type));
245 * $query->execute();
246 *
247 * // Gets all of the destinations.
248 * $all_phids = $query->getDestinationPHIDs();
249 * $handles = id(new PhabricatorHandleQuery())
250 * ->setViewer($viewer)
251 * ->withPHIDs($all_phids)
252 * ->execute();
253 *
254 * foreach ($objects as $object) {
255 * // Get all of the destinations for the given object.
256 * $dst_phids = $query->getDestinationPHIDs(array($object->getPHID()));
257 * $object->attachHandles(array_select_keys($handles, $dst_phids));
258 * }
259 *
260 * @param list $src_phids (optional) List of PHIDs to select, or empty to
261 * select all.
262 * @param list $types (optional) List of edge types to select, or empty to
263 * select all.
264 * @return list<string> List of matching destination PHIDs.
265 */
266 public function getDestinationPHIDs(
267 array $src_phids = array(),
268 array $types = array()) {
269 if ($this->resultSet === null) {
270 throw new PhutilInvalidStateException('execute');
271 }
272
273 $result_phids = array();
274
275 $set = $this->resultSet;
276 if ($src_phids) {
277 $set = array_select_keys($set, $src_phids);
278 }
279
280 foreach ($set as $src => $edges_by_type) {
281 if ($types) {
282 $edges_by_type = array_select_keys($edges_by_type, $types);
283 }
284
285 foreach ($edges_by_type as $edges) {
286 foreach ($edges as $edge_phid => $edge) {
287 $result_phids[$edge_phid] = true;
288 }
289 }
290 }
291
292 return array_keys($result_phids);
293 }
294
295
296/* -( Internals )---------------------------------------------------------- */
297
298
299 /**
300 * @task internal
301 */
302 protected function buildWhereClause(AphrontDatabaseConnection $conn) {
303 $where = array();
304
305 if ($this->sourcePHIDs) {
306 $where[] = qsprintf(
307 $conn,
308 'edge.src IN (%Ls)',
309 $this->sourcePHIDs);
310 }
311
312 if ($this->edgeTypes) {
313 $where[] = qsprintf(
314 $conn,
315 'edge.type IN (%Ls)',
316 $this->edgeTypes);
317 }
318
319 if ($this->destPHIDs) {
320 // potentially complain if $this->edgeType was not set
321 $where[] = qsprintf(
322 $conn,
323 'edge.dst IN (%Ls)',
324 $this->destPHIDs);
325 }
326
327 return $this->formatWhereClause($conn, $where);
328 }
329
330
331 /**
332 * @task internal
333 */
334 private function buildOrderClause(AphrontDatabaseConnection $conn) {
335 if ($this->order == self::ORDER_NEWEST_FIRST) {
336 return qsprintf($conn, 'ORDER BY edge.dateCreated DESC, edge.seq DESC');
337 } else {
338 return qsprintf($conn, 'ORDER BY edge.dateCreated ASC, edge.seq ASC');
339 }
340 }
341
342}