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