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