Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 115 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
RootPostLoader | |
0.00% |
0 / 115 |
|
0.00% |
0 / 6 |
1056 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getWithRoot | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
get | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getMulti | |
0.00% |
0 / 74 |
|
0.00% |
0 / 1 |
380 | |||
fetchRelatedPostIds | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getTreeRepo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Flow\Repository; |
4 | |
5 | use Flow\Data\ManagerGroup; |
6 | use Flow\Exception\InvalidDataException; |
7 | use Flow\Model\PostRevision; |
8 | use Flow\Model\UUID; |
9 | use FormatJson; |
10 | use MediaWiki\User\User; |
11 | |
12 | /** |
13 | * I'm pretty sure this will generally work for any subtree, not just the topic |
14 | * root. The problem is once you allow any subtree you need to handle the |
15 | * depth and root post setters better, they make the assumption the root provided |
16 | * is actually a root. |
17 | */ |
18 | class RootPostLoader { |
19 | /** |
20 | * @var ManagerGroup |
21 | */ |
22 | protected $storage; |
23 | |
24 | /** |
25 | * @var TreeRepository |
26 | */ |
27 | protected $treeRepo; |
28 | |
29 | /** |
30 | * @param ManagerGroup $storage |
31 | * @param TreeRepository $treeRepo |
32 | */ |
33 | public function __construct( ManagerGroup $storage, TreeRepository $treeRepo ) { |
34 | $this->storage = $storage; |
35 | $this->treeRepo = $treeRepo; |
36 | } |
37 | |
38 | /** |
39 | * Retrieves a single post and the related topic title. |
40 | * |
41 | * @param UUID|string $postId The uid of the post being requested |
42 | * @return (PostRevision|null)[] associative array with 'root' and 'post' keys. Array |
43 | * values may be null if not found. |
44 | * @throws InvalidDataException |
45 | * @phan-return array{root:null|PostRevision,post:null|PostRevision} |
46 | */ |
47 | public function getWithRoot( $postId ) { |
48 | $postId = UUID::create( $postId ); |
49 | $rootId = $this->treeRepo->findRoot( $postId ); |
50 | $found = $this->storage->findMulti( |
51 | 'PostRevision', |
52 | [ |
53 | [ 'rev_type_id' => $postId ], |
54 | [ 'rev_type_id' => $rootId ], |
55 | ], |
56 | [ 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ] |
57 | ); |
58 | $res = [ |
59 | 'post' => null, |
60 | 'root' => null, |
61 | ]; |
62 | if ( !$found ) { |
63 | return $res; |
64 | } |
65 | foreach ( $found as $result ) { |
66 | // limit = 1 means single result |
67 | $post = reset( $result ); |
68 | if ( $postId->equals( $post->getPostId() ) ) { |
69 | $res['post'] = $post; |
70 | } elseif ( $rootId->equals( $post->getPostId() ) ) { |
71 | $res['root'] = $post; |
72 | } else { |
73 | throw new InvalidDataException( 'Unmatched: ' . $post->getPostId()->getAlphadecimal() ); |
74 | } |
75 | } |
76 | // The above doesn't catch this condition |
77 | if ( $postId->equals( $rootId ) ) { |
78 | $res['root'] = $res['post']; |
79 | } |
80 | return $res; |
81 | } |
82 | |
83 | /** |
84 | * @param UUID $topicId |
85 | * @return PostRevision |
86 | * @throws InvalidDataException |
87 | */ |
88 | public function get( $topicId ) { |
89 | $result = $this->getMulti( [ $topicId ] ); |
90 | return reset( $result ); |
91 | } |
92 | |
93 | /** |
94 | * @param UUID[] $topicIds |
95 | * @return PostRevision[] |
96 | * @throws InvalidDataException |
97 | */ |
98 | public function getMulti( array $topicIds ) { |
99 | if ( !$topicIds ) { |
100 | return []; |
101 | } |
102 | // load posts for all located post ids |
103 | $allPostIds = $this->fetchRelatedPostIds( $topicIds ); |
104 | $queries = []; |
105 | foreach ( $allPostIds as $postId ) { |
106 | $queries[] = [ 'rev_type_id' => $postId ]; |
107 | } |
108 | $found = $this->storage->findMulti( 'PostRevision', $queries, [ |
109 | 'sort' => 'rev_id', |
110 | 'order' => 'DESC', |
111 | 'limit' => 1, |
112 | ] ); |
113 | /** @var PostRevision[] $posts */ |
114 | $posts = $children = []; |
115 | foreach ( $found as $indexResult ) { |
116 | $post = reset( $indexResult ); // limit => 1 means only 1 result per query |
117 | if ( isset( $posts[$post->getPostId()->getAlphadecimal()] ) ) { |
118 | throw new InvalidDataException( |
119 | 'Multiple results for id: ' . $post->getPostId()->getAlphadecimal(), |
120 | 'fail-load-data' |
121 | ); |
122 | } |
123 | $posts[$post->getPostId()->getAlphadecimal()] = $post; |
124 | } |
125 | $prettyPostIds = []; |
126 | foreach ( $allPostIds as $id ) { |
127 | $prettyPostIds[] = $id->getAlphadecimal(); |
128 | } |
129 | $missing = array_diff( $prettyPostIds, array_keys( $posts ) ); |
130 | if ( $missing ) { |
131 | // convert string uuid's into UUID objects |
132 | /** @var UUID[] $missingUUID */ |
133 | $missingUUID = array_map( [ UUID::class, 'create' ], $missing ); |
134 | |
135 | // we'll need to know parents to add stub post correctly in post hierarchy |
136 | $parents = $this->treeRepo->fetchParentMap( $missingUUID ); |
137 | $missingParents = array_diff( $missing, array_keys( $parents ) ); |
138 | if ( $missingParents ) { |
139 | // if we can't fetch a post's original position in the tree |
140 | // hierarchy, we can't create a stub post to display, so bail |
141 | throw new InvalidDataException( |
142 | 'Missing Posts & parents: ' . json_encode( $missingParents ), |
143 | 'fail-load-data' |
144 | ); |
145 | } |
146 | |
147 | foreach ( $missingUUID as $postId ) { |
148 | $content = wfMessage( 'flow-stub-post-content' )->text(); |
149 | $username = wfMessage( 'flow-system-usertext' )->text(); |
150 | $user = User::newFromName( $username ); |
151 | |
152 | // create a stub post instead of failing completely |
153 | $post = PostRevision::newFromId( $postId, $user, $content, 'wikitext' ); |
154 | $post->setReplyToId( $parents[$postId->getAlphadecimal()] ); |
155 | $posts[$postId->getAlphadecimal()] = $post; |
156 | |
157 | wfDebugLog( 'Flow', __METHOD__ . ': Missing posts: ' . FormatJson::encode( $missing ) ); |
158 | } |
159 | } |
160 | // another helper to catch bugs in dev |
161 | $extra = array_diff( array_keys( $posts ), $prettyPostIds ); |
162 | if ( $extra ) { |
163 | throw new InvalidDataException( |
164 | 'Found unrequested posts: ' . FormatJson::encode( $extra ), |
165 | 'fail-load-data' |
166 | ); |
167 | } |
168 | |
169 | // populate array of children |
170 | foreach ( $posts as $post ) { |
171 | if ( $post->getReplyToId() ) { |
172 | $children[$post->getReplyToId()->getAlphadecimal()][$post->getPostId()->getAlphadecimal()] = $post; |
173 | } |
174 | } |
175 | $extraParents = array_diff( array_keys( $children ), $prettyPostIds ); |
176 | if ( $extraParents ) { |
177 | throw new InvalidDataException( |
178 | 'Found posts with unrequested parents: ' . FormatJson::encode( $extraParents ), |
179 | 'fail-load-data' |
180 | ); |
181 | } |
182 | |
183 | foreach ( $posts as $postId => $post ) { |
184 | $postChildren = []; |
185 | $postDepth = 0; |
186 | |
187 | // link parents to their children |
188 | if ( isset( $children[$postId] ) ) { |
189 | // sort children with oldest items first |
190 | ksort( $children[$postId] ); |
191 | $postChildren = $children[$postId]; |
192 | } |
193 | |
194 | // determine threading depth of post |
195 | $replyToId = $post->getReplyToId(); |
196 | while ( $replyToId && isset( $children[$replyToId->getAlphadecimal()] ) ) { |
197 | $postDepth++; |
198 | $replyToId = $posts[$replyToId->getAlphadecimal()]->getReplyToId(); |
199 | } |
200 | |
201 | $post->setChildren( $postChildren ); |
202 | $post->setDepth( $postDepth ); |
203 | } |
204 | |
205 | // return only the requested posts, rest are available as children. |
206 | // Return in same order as requested |
207 | /** @var PostRevision[] $roots */ |
208 | $roots = []; |
209 | foreach ( $topicIds as $id ) { |
210 | $roots[$id->getAlphadecimal()] = $posts[$id->getAlphadecimal()]; |
211 | } |
212 | // Attach every post in the tree to its root. setRootPost |
213 | // recursively applies it to all children as well. |
214 | foreach ( $roots as $post ) { |
215 | $post->setRootPost( $post ); |
216 | } |
217 | return $roots; |
218 | } |
219 | |
220 | /** |
221 | * @param UUID[] $postIds |
222 | * @return UUID[] Map from alphadecimal id to UUID object |
223 | */ |
224 | protected function fetchRelatedPostIds( array $postIds ) { |
225 | // list of all posts descendant from the provided $postIds |
226 | $nodeList = $this->treeRepo->fetchSubtreeNodeList( $postIds ); |
227 | // merge all the children from the various posts into one array |
228 | if ( !$nodeList ) { |
229 | // It should have returned at least $postIds |
230 | // TODO: log errors? |
231 | $res = $postIds; |
232 | } elseif ( count( $nodeList ) === 1 ) { |
233 | $res = reset( $nodeList ); |
234 | } else { |
235 | $res = array_merge( ...array_values( $nodeList ) ); |
236 | } |
237 | |
238 | $retval = []; |
239 | foreach ( $res as $id ) { |
240 | $retval[$id->getAlphadecimal()] = $id; |
241 | } |
242 | return $retval; |
243 | } |
244 | |
245 | /** |
246 | * @return TreeRepository |
247 | */ |
248 | public function getTreeRepo() { |
249 | return $this->treeRepo; |
250 | } |
251 | } |