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    /**
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}