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