Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
HistoryQuery
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 4
90
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
 includeInHistory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOptions
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 doInternalQueries
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace Flow\Formatter;
4
5use Flow\Data\ManagerGroup;
6use Flow\FlowActions;
7use Flow\Model\AbstractRevision;
8use Flow\Model\UUID;
9use Flow\Repository\TreeRepository;
10
11abstract class HistoryQuery extends AbstractQuery {
12    // This requests extra to take into account that we will filter some out,
13    // to try to reduce the number of rounds (preferably to 1).
14    // If you raise this, also increase FLOW_HISTORY_INDEX_LIMIT and bump the
15    // key of the indexes using FLOW_HISTORY_INDEX_LIMIT
16    // This magic number is based on new-post/new-topic being about 26% of post revisions.
17    // (queried from production), since that is the only thing currently excluded.
18    protected const POST_OVERFETCH_FACTOR = 1.36;
19
20    /**
21     * @var FlowActions
22     */
23    protected $actions;
24
25    /**
26     * @param ManagerGroup $storage
27     * @param TreeRepository $treeRepo
28     * @param FlowActions $actions
29     */
30    public function __construct(
31        ManagerGroup $storage,
32        TreeRepository $treeRepo,
33        FlowActions $actions
34    ) {
35        parent::__construct( $storage, $treeRepo );
36        $this->actions = $actions;
37    }
38
39    /**
40     * @param AbstractRevision $revision
41     * @return bool
42     */
43    protected function includeInHistory( AbstractRevision $revision ) {
44        // If you add exclude_from_history to a new type, use doInternalQueries on additional
45        // queries as needed.
46        return !$this->actions->getValue( $revision->getChangeType(), 'exclude_from_history' );
47    }
48
49    /**
50     * Gets query options that are common to all history queries
51     *
52     * @param string $direction 'fwd' or 'rev'.  'fwd' means to get items older than
53     *  the offset.  'rev' means to get items newer.  Either way, an individual page is
54     *  eventually returned and displayed in descending order.
55     * @param int $limit Maximum number of items
56     * @param UUID|null $offset UUID to use as offset (optional)
57     * @return array Associative array of options for query
58     */
59    protected function getOptions( $direction, $limit, ?UUID $offset = null ) {
60        return [
61            'sort' => 'rev_id',
62            'order' => $direction === 'fwd' ? 'DESC' : 'ASC',
63            'limit' => $limit,
64            'offset-id' => $offset,
65            'offset-dir' => $direction,
66            'offset-include' => false,
67        ];
68    }
69
70    /**
71     * Internally re-query as needed to handle items excluded from history
72     *
73     * Re-queries until there are no more entries or after filtering, there are the
74     * desired number of results.
75     *
76     * This respects the given order (ASC or DESC), but the reversing for 'rev' is in
77     * getResults.
78     *
79     * @param string $storageClass Storage class ID
80     * @param array $attributes Query attriutes
81     * @param array $options Query options, including offset-id and limit
82     * @param float $overfetchFactor Factor to overfetch by to anticipate excludes
83     * @return array Array of history rows
84     */
85    protected function doInternalQueries( $storageClass, $attributes, $options, $overfetchFactor ) {
86        $result = [];
87
88        $limit = $options['limit'];
89        $internalOffset = $options['offset-id'];
90
91        do {
92            $remainingNeeded = $limit - count( $result );
93
94            // The special cases here are to try reduce dribbling out of final requests (50, 25, 10, 5...).
95            if ( $remainingNeeded < 50 ) {
96                $overfetchFactor *= 2;
97            }
98
99            $beforeFilteringCountWanted = max( 10, intval( $overfetchFactor * $remainingNeeded ) );
100
101            // Over-fetch by 1 item so we can figure out when to stop re-querying.
102            $options['limit'] = $beforeFilteringCountWanted + 1;
103
104            $options['offset-id'] = $internalOffset;
105
106            $resultBeforeFiltering = $this->storage->find( $storageClass, $attributes, $options );
107
108            // We over-fetched, now get rid of redundant value for our "real" data
109            $internalOverfetched = null;
110            if ( count( $resultBeforeFiltering ) > $beforeFilteringCountWanted ) {
111                $internalOverfetched = array_pop( $resultBeforeFiltering );
112            }
113
114            $resultAfterFiltering = array_filter( $resultBeforeFiltering, [ $this, 'includeInHistory' ] );
115
116            if ( count( $resultBeforeFiltering ) >= 1 ) {
117                $internalOffset = end( $resultBeforeFiltering )->getRevisionId();
118            }
119
120            $trimmedResultAfterFiltering = array_slice( $resultAfterFiltering, 0, $remainingNeeded );
121            $result = array_merge( $result, $trimmedResultAfterFiltering );
122        } while ( count( $result ) < $limit && $internalOverfetched !== null );
123
124        return $result;
125    }
126
127}