Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.06% covered (danger)
31.06%
41 / 132
6.25% covered (danger)
6.25%
1 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
ObjectLocator
31.06% covered (danger)
31.06%
41 / 132
6.25% covered (danger)
6.25%
1 / 16
1406.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getMapper
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 find
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 findMulti
57.14% covered (warning)
57.14%
16 / 28
0.00% covered (danger)
0.00%
0 / 1
15.38
 found
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 foundMulti
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 getPrimaryKeyColumns
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getMulti
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 got
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 gotMulti
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 clear
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexFor
52.94% covered (warning)
52.94%
9 / 17
0.00% covered (danger)
0.00%
0 / 1
14.67
 load
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 convertToDbOptions
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
8.60
 convertToDbQueries
26.67% covered (danger)
26.67%
4 / 15
0.00% covered (danger)
0.00%
0 / 1
49.44
1<?php
2
3namespace Flow\Data;
4
5use Flow\DbFactory;
6use Flow\Exception\NoIndexException;
7use Flow\Model\UUID;
8use FormatJson;
9
10/**
11 * Denormalized indexes that are query-only.  The indexes used here must
12 * be provided to some ObjectManager as a lifecycleHandler to receive
13 * update events.
14 */
15class ObjectLocator {
16    /**
17     * @var ObjectMapper
18     */
19    protected $mapper;
20
21    /**
22     * @var ObjectStorage
23     */
24    protected $storage;
25
26    /**
27     * @var Index[]
28     */
29    protected $indexes;
30
31    /**
32     * Database factory (only for addQuotes)
33     *
34     * @var DbFactory
35     */
36    protected $dbFactory;
37
38    /**
39     * @var LifecycleHandler[]
40     */
41    protected $lifecycleHandlers;
42
43    /**
44     * @param ObjectMapper $mapper
45     * @param ObjectStorage $storage
46     * @param DbFactory $dbFactory
47     * @param Index[] $indexes
48     * @param LifecycleHandler[] $lifecycleHandlers
49     */
50    public function __construct(
51        ObjectMapper $mapper,
52        ObjectStorage $storage,
53        DbFactory $dbFactory,
54        array $indexes = [],
55        array $lifecycleHandlers = []
56    ) {
57        $this->mapper = $mapper;
58        $this->storage = $storage;
59        $this->indexes = $indexes;
60        $this->dbFactory = $dbFactory;
61        $this->lifecycleHandlers = array_merge( $indexes, $lifecycleHandlers );
62    }
63
64    public function getMapper() {
65        return $this->mapper;
66    }
67
68    public function find( array $attributes, array $options = [] ) {
69        $result = $this->findMulti( [ $attributes ], $options );
70        return $result ? reset( $result ) : [];
71    }
72
73    /**
74     * All queries must be against the same index. Results are equivalent to
75     * array_map, maintaining order and key relationship between input $queries
76     * and $result.
77     *
78     * @param array $queries
79     * @param array $options
80     * @return array[]
81     */
82    public function findMulti( array $queries, array $options = [] ) {
83        if ( !$queries ) {
84            return [];
85        }
86
87        $keys = array_keys( reset( $queries ) );
88        if ( isset( $options['sort'] ) && !is_array( $options['sort'] ) ) {
89            $options['sort'] = ObjectManager::makeArray( $options['sort'] );
90        }
91
92        try {
93            $index = $this->getIndexFor( $keys, $options );
94            $res = $index->findMulti( $queries, $options );
95        } catch ( NoIndexException $e ) {
96            if ( array_search( 'topic_root_id', $keys ) ) {
97                wfDebugLog(
98                    'Flow',
99                    __METHOD__ . ': '
100                    . json_encode( $keys ) . ' : '
101                    . json_encode( $options ) . ' : '
102                    . json_encode( array_map( 'get_class', $this->indexes ) )
103                );
104                \MWExceptionHandler::logException( $e );
105            } else {
106                wfDebugLog( 'FlowDebug', __METHOD__ . ': ' . $e->getMessage() );
107            }
108            $res = $this->storage->findMulti(
109                $this->convertToDbQueries( $queries, $options ),
110                $this->convertToDbOptions( $options )
111            );
112        }
113
114        $output = [];
115        foreach ( $res as $index => $queryOutput ) {
116            foreach ( $queryOutput as $k => $v ) {
117                if ( $v ) {
118                    $output[$index][$k] = $this->load( $v );
119                }
120            }
121        }
122
123        return $output;
124    }
125
126    /**
127     * Returns a boolean true/false if the find()-operation for the given
128     * attributes has already been resolves and doesn't need to query any
129     * outside cache/database.
130     * Determining if a find() has not yet been resolved may be useful so that
131     * additional data may be loaded at once.
132     *
133     * @param array $attributes Attributes to find()
134     * @param array $options Options to find()
135     * @return bool
136     */
137    public function found( array $attributes, array $options = [] ) {
138        return $this->foundMulti( [ $attributes ], $options );
139    }
140
141    /**
142     * Returns a boolean true/false if the findMulti()-operation for the given
143     * attributes has already been resolves and doesn't need to query any
144     * outside cache/database.
145     * Determining if a find() has not yet been resolved may be useful so that
146     * additional data may be loaded at once.
147     *
148     * @param array $queries Queries to findMulti()
149     * @param array $options Options to findMulti()
150     * @return bool
151     */
152    public function foundMulti( array $queries, array $options = [] ) {
153        if ( !$queries ) {
154            return true;
155        }
156
157        $keys = array_keys( reset( $queries ) );
158        if ( isset( $options['sort'] ) && !is_array( $options['sort'] ) ) {
159            $options['sort'] = ObjectManager::makeArray( $options['sort'] );
160        }
161
162        foreach ( $queries as $key => $value ) {
163            $queries[$key] = UUID::convertUUIDs( $value, 'alphadecimal' );
164        }
165
166        try {
167            $index = $this->getIndexFor( $keys, $options );
168            $res = $index->foundMulti( $queries, $options );
169            return $res;
170        } catch ( NoIndexException $e ) {
171            wfDebugLog( 'FlowDebug', __METHOD__ . ': ' . $e->getMessage() );
172        }
173
174        return false;
175    }
176
177    public function getPrimaryKeyColumns() {
178        return $this->storage->getPrimaryKeyColumns();
179    }
180
181    public function get( $id ) {
182        $result = $this->getMulti( [ $id ] );
183        return $result ? reset( $result ) : null;
184    }
185
186    /**
187     * Just a helper to find by primary key
188     * Be careful with regards to order on composite primary keys,
189     * must be in same order as provided to the storage implementation.
190     * @param array $objectIds
191     * @return array
192     */
193    public function getMulti( array $objectIds ) {
194        if ( !$objectIds ) {
195            return [];
196        }
197        $primaryKey = $this->storage->getPrimaryKeyColumns();
198        $queries = [];
199        $retval = [];
200        foreach ( $objectIds as $id ) {
201            // check internal cache
202            $query = array_combine( $primaryKey, ObjectManager::makeArray( $id ) );
203            $obj = $this->mapper->get( $query );
204            if ( $obj === null ) {
205                $queries[] = $query;
206            } else {
207                $retval[] = $obj;
208            }
209        }
210        if ( $queries ) {
211            $res = $this->findMulti( $queries );
212            if ( $res ) {
213                foreach ( $res as $row ) {
214                    // primary key is unique, but indexes still return their results as array
215                    // to be consistent. undo that for a flat result array
216                    $retval[] = reset( $row );
217                }
218            }
219        }
220
221        return $retval;
222    }
223
224    /**
225     * Returns a boolean true/false if the get()-operation for the given
226     * attributes has already been resolves and doesn't need to query any
227     * outside cache/database.
228     * Determining if a find() has not yet been resolved may be useful so that
229     * additional data may be loaded at once.
230     *
231     * @param string|int $id Id to get()
232     * @return bool
233     */
234    public function got( $id ) {
235        return $this->gotMulti( [ $id ] );
236    }
237
238    /**
239     * Returns a boolean true/false if the getMulti()-operation for the given
240     * attributes has already been resolves and doesn't need to query any
241     * outside cache/database.
242     * Determining if a find() has not yet been resolved may be useful so that
243     * additional data may be loaded at once.
244     *
245     * @param array $objectIds Ids to getMulti()
246     * @return bool
247     */
248    public function gotMulti( array $objectIds ) {
249        if ( !$objectIds ) {
250            return true;
251        }
252
253        $primaryKey = $this->storage->getPrimaryKeyColumns();
254        $queries = [];
255        foreach ( $objectIds as $id ) {
256            $query = array_combine( $primaryKey, ObjectManager::makeArray( $id ) );
257            $query = UUID::convertUUIDs( $query, 'alphadecimal' );
258            if ( !$this->mapper->get( $query ) ) {
259                $queries[] = $query;
260            }
261        }
262
263        if ( $queries && $this->mapper instanceof Mapper\CachingObjectMapper ) {
264            return false;
265        }
266
267        return $this->foundMulti( $queries );
268    }
269
270    public function clear() {
271        // nop, we don't store anything
272    }
273
274    /**
275     * @param array $keys
276     * @param array $options
277     * @return Index
278     * @throws NoIndexException
279     */
280    public function getIndexFor( array $keys, array $options = [] ) {
281        sort( $keys );
282        /** @var Index|null $current */
283        $current = null;
284        foreach ( $this->indexes as $index ) {
285            // @var Index $index
286            if ( !$index->canAnswer( $keys, $options ) ) {
287                continue;
288            }
289
290            // make sure at least some index is picked
291            if ( $current === null ) {
292                $current = $index;
293
294            // Find the smallest matching index
295            } elseif ( isset( $options['limit'] ) ) {
296                $current = $index->getLimit() < $current->getLimit() ? $index : $current;
297
298            // if no limit specified, find biggest matching index
299            } else {
300                $current = $index->getLimit() > $current->getLimit() ? $index : $current;
301            }
302        }
303        if ( $current === null ) {
304            $count = count( $this->indexes );
305            throw new NoIndexException(
306                "No index (out of $count) available to answer query for " . implode( ", ", $keys ) .
307                ' with options ' . FormatJson::encode( $options ), 'no-index'
308            );
309        }
310        return $current;
311    }
312
313    protected function load( array $row ) {
314        $object = $this->mapper->fromStorageRow( $row );
315        foreach ( $this->lifecycleHandlers as $handler ) {
316            $handler->onAfterLoad( $object, $row );
317        }
318        return $object;
319    }
320
321    /**
322     * Convert index options to db equivalent options
323     * @param array $options
324     * @return array
325     */
326    protected function convertToDbOptions( array $options ) {
327        $dbOptions = $orderBy = [];
328        $order = '';
329
330        if ( isset( $options['limit'] ) ) {
331            $dbOptions['LIMIT'] = (int)$options['limit'];
332        }
333
334        if ( isset( $options['order'] ) ) {
335            $order = ' ' . $options['order'];
336        }
337
338        if ( isset( $options['sort'] ) ) {
339            foreach ( $options['sort'] as $val ) {
340                $orderBy[] = $val . $order;
341            }
342        }
343
344        if ( $orderBy ) {
345            $dbOptions['ORDER BY'] = $orderBy;
346        }
347
348        return $dbOptions;
349    }
350
351    /**
352     * Uses options to figure out conditions to add to the DB queries.
353     *
354     * @param array[] $queries Array of queries, with each element an array of attributes
355     * @param array $options Options for queries
356     * @return array Queries for BasicDbStorage class
357     */
358    protected function convertToDbQueries( array $queries, array $options ) {
359        if ( isset( $options['offset-id'] ) &&
360            isset( $options['sort'] ) && count( $options['sort'] ) === 1 &&
361            preg_match( '/_id$/', $options['sort'][0] )
362        ) {
363            if ( !$options['offset-id'] instanceof UUID ) {
364                $options['offset-id'] = UUID::create( $options['offset-id'] );
365            }
366
367            if ( $options['order'] === 'ASC' ) {
368                $operator = '>';
369            } else {
370                $operator = '<';
371            }
372
373            if ( isset( $options['offset-include'] ) && $options['offset-include'] ) {
374                $operator .= '=';
375            }
376
377            $dbr = $this->dbFactory->getDB( DB_REPLICA );
378            $condition = $dbr->expr( $options['sort'][0], $operator, $options['offset-id']->getBinary() );
379
380            foreach ( $queries as &$query ) {
381                $query[] = $condition;
382            }
383        }
384
385        return $queries;
386    }
387}