1: <?php
2:
3: namespace RedBeanPHP;
4:
5:
6: /**
7: * RedBeanPHP Finder.
8: * Service class to find beans. For the most part this class
9: * offers user friendly utility methods for interacting with the
10: * OODB::find() method, which is rather complex. This class can be
11: * used to find beans using plain old SQL queries.
12: *
13: * @file RedBeanPHP/Finder.php
14: * @author Gabor de Mooij and the RedBeanPHP Community
15: * @license BSD/GPLv2
16: *
17: * @copyright
18: * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community
19: * This source file is subject to the BSD/GPLv2 License that is bundled
20: * with this source code in the file license.txt.
21: */
22: class Finder
23: {
24: /**
25: * @var ToolBox
26: */
27: protected $toolbox;
28:
29: /**
30: * @var OODB
31: */
32: protected $redbean;
33:
34: /**
35: * Constructor.
36: * The Finder requires a toolbox.
37: *
38: * @param ToolBox $toolbox
39: */
40: public function __construct( ToolBox $toolbox )
41: {
42: $this->toolbox = $toolbox;
43: $this->redbean = $toolbox->getRedBean();
44: }
45:
46: /**
47: * Finds a bean using a type and a where clause (SQL).
48: * As with most Query tools in RedBean you can provide values to
49: * be inserted in the SQL statement by populating the value
50: * array parameter; you can either use the question mark notation
51: * or the slot-notation (:keyname).
52: *
53: * @param string $type type the type of bean you are looking for
54: * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause
55: * @param array $bindings values array of values to be bound to parameters in query
56: *
57: * @return array
58: */
59: public function find( $type, $sql = NULL, $bindings = array() )
60: {
61: if ( !is_array( $bindings ) ) {
62: throw new RedException(
63: 'Expected array, ' . gettype( $bindings ) . ' given.'
64: );
65: }
66:
67: return $this->redbean->find( $type, array(), $sql, $bindings );
68: }
69:
70: /**
71: * Like find() but also exports the beans as an array.
72: * This method will perform a find-operation. For every bean
73: * in the result collection this method will call the export() method.
74: * This method returns an array containing the array representations
75: * of every bean in the result set.
76: *
77: * @see Finder::find
78: *
79: * @param string $type type the type of bean you are looking for
80: * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause
81: * @param array $bindings values array of values to be bound to parameters in query
82: *
83: * @return array
84: */
85: public function findAndExport( $type, $sql = NULL, $bindings = array() )
86: {
87: $arr = array();
88: foreach ( $this->find( $type, $sql, $bindings ) as $key => $item ) {
89: $arr[] = $item->export();
90: }
91:
92: return $arr;
93: }
94:
95: /**
96: * Like find() but returns just one bean instead of an array of beans.
97: * This method will return only the first bean of the array.
98: * If no beans are found, this method will return NULL.
99: *
100: * @see Finder::find
101: *
102: * @param string $type type the type of bean you are looking for
103: * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause
104: * @param array $bindings values array of values to be bound to parameters in query
105: *
106: * @return OODBBean
107: */
108: public function findOne( $type, $sql = NULL, $bindings = array() )
109: {
110: $sql = $this->toolbox->getWriter()->glueLimitOne( $sql );
111:
112: $items = $this->find( $type, $sql, $bindings );
113:
114: if ( empty($items) ) {
115: return NULL;
116: }
117:
118: return reset( $items );
119: }
120:
121: /**
122: * Like find() but returns the last bean of the result array.
123: * Opposite of Finder::findLast().
124: * If no beans are found, this method will return NULL.
125: *
126: * @see Finder::find
127: *
128: * @param string $type the type of bean you are looking for
129: * @param string $sql SQL query to find the desired bean, starting right after WHERE clause
130: * @param array $bindings values array of values to be bound to parameters in query
131: *
132: * @return OODBBean
133: */
134: public function findLast( $type, $sql = NULL, $bindings = array() )
135: {
136: $items = $this->find( $type, $sql, $bindings );
137:
138: if ( empty($items) ) {
139: return NULL;
140: }
141:
142: return end( $items );
143: }
144:
145: /**
146: * Tries to find beans of a certain type,
147: * if no beans are found, it dispenses a bean of that type.
148: *
149: * @see Finder::find
150: *
151: * @param string $type the type of bean you are looking for
152: * @param string $sql SQL query to find the desired bean, starting right after WHERE clause
153: * @param array $bindings values array of values to be bound to parameters in query
154: *
155: * @return array
156: */
157: public function findOrDispense( $type, $sql = NULL, $bindings = array() )
158: {
159: $foundBeans = $this->find( $type, $sql, $bindings );
160:
161: if ( empty( $foundBeans ) ) {
162: return array( $this->redbean->dispense( $type ) );
163: } else {
164: return $foundBeans;
165: }
166: }
167:
168: /**
169: * Finds a BeanCollection using the repository.
170: * A bean collection can be used to retrieve one bean at a time using
171: * cursors - this is useful for processing large datasets. A bean collection
172: * will not load all beans into memory all at once, just one at a time.
173: *
174: * @param string $type the type of bean you are looking for
175: * @param string $sql SQL query to find the desired bean, starting right after WHERE clause
176: * @param array $bindings values array of values to be bound to parameters in query
177: *
178: * @return BeanCollection
179: */
180: public function findCollection( $type, $sql, $bindings = array() )
181: {
182: return $this->redbean->findCollection( $type, $sql, $bindings );
183: }
184:
185: /**
186: * Finds or creates a bean.
187: * Tries to find a bean with certain properties specified in the second
188: * parameter ($like). If the bean is found, it will be returned.
189: * If multiple beans are found, only the first will be returned.
190: * If no beans match the criteria, a new bean will be dispensed,
191: * the criteria will be imported as properties and this new bean
192: * will be stored and returned.
193: *
194: * Format of criteria set: property => value
195: * The criteria set also supports OR-conditions: property => array( value1, orValue2 )
196: *
197: * @param string $type type of bean to search for
198: * @param array $like criteria set describing bean to search for
199: *
200: * @return OODBBean
201: */
202: public function findOrCreate( $type, $like = array() )
203: {
204: $beans = $this->findLike( $type, $like );
205: if ( count( $beans ) ) {
206: $bean = reset( $beans );
207: return $bean;
208: }
209:
210: $bean = $this->redbean->dispense( $type );
211: $bean->import( $like );
212: $this->redbean->store( $bean );
213: return $bean;
214: }
215:
216: /**
217: * Finds beans by its type and a certain criteria set.
218: *
219: * Format of criteria set: property => value
220: * The criteria set also supports OR-conditions: property => array( value1, orValue2 )
221: *
222: * If the additional SQL is a condition, this condition will be glued to the rest
223: * of the query using an AND operator. Note that this is as far as this method
224: * can go, there is no way to glue additional SQL using an OR-condition.
225: * This method provides access to an underlying mechanism in the RedBeanPHP architecture
226: * to find beans using criteria sets. However, please do not use this method
227: * for complex queries, use plain SQL instead ( the regular find method ) as it is
228: * more suitable for the job. This method is
229: * meant for basic search-by-example operations.
230: *
231: * @param string $type type of bean to search for
232: * @param array $conditions criteria set describing the bean to search for
233: * @param string $sql additional SQL (for sorting)
234: *
235: * @return array
236: */
237: public function findLike( $type, $conditions = array(), $sql = '' )
238: {
239: if ( count( $conditions ) > 0 ) {
240: foreach( $conditions as $key => $condition ) {
241: if ( !count( $condition ) ) unset( $conditions[$key] );
242: }
243: }
244:
245: return $this->redbean->find( $type, $conditions, $sql );
246: }
247:
248: /**
249: * Returns a hashmap with bean arrays keyed by type using an SQL
250: * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review'
251: * this method will return movie and review beans.
252: *
253: * Example:
254: *
255: * <code>
256: * $stuff = $finder->findMulti('movie,review', '
257: * SELECT movie.*, review.* FROM movie
258: * LEFT JOIN review ON review.movie_id = movie.id');
259: * </code>
260: *
261: * After this operation, $stuff will contain an entry 'movie' containing all
262: * movies and an entry named 'review' containing all reviews (all beans).
263: * You can also pass bindings.
264: *
265: * If you want to re-map your beans, so you can use $movie->ownReviewList without
266: * having RedBeanPHP executing an SQL query you can use the fourth parameter to
267: * define a selection of remapping closures.
268: *
269: * The remapping argument (optional) should contain an array of arrays.
270: * Each array in the remapping array should contain the following entries:
271: *
272: * <code>
273: * array(
274: * 'a' => TYPE A
275: * 'b' => TYPE B
276: * 'matcher' => MATCHING FUNCTION ACCEPTING A, B and ALL BEANS
277: * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS
278: * )
279: * </code>
280: *
281: * Using this mechanism you can build your own 'preloader' with tiny function
282: * snippets (and those can be re-used and shared online of course).
283: *
284: * Example:
285: *
286: * <code>
287: * array(
288: * 'a' => 'movie' //define A as movie
289: * 'b' => 'review' //define B as review
290: * 'matcher' => function( $a, $b ) {
291: * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id
292: * }
293: * 'do' => function( $a, $b ) {
294: * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie
295: * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'.
296: * }
297: * )
298: * </code>
299: *
300: * The Query Template parameter is optional as well but can be used to
301: * set a different SQL template (sprintf-style) for processing the original query.
302: *
303: * @note the SQL query provided IS NOT THE ONE used internally by this function,
304: * this function will pre-process the query to get all the data required to find the beans.
305: *
306: * @note if you use the 'book.*' notation make SURE you're
307: * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because
308: * it's actually an SQL-like template SLOT, not real SQL.
309: *
310: * @note instead of an SQL query you can pass a result array as well.
311: *
312: * @param string|array $types a list of types (either array or comma separated string)
313: * @param string|array $sqlOrArr an SQL query or an array of prefetched records
314: * @param array $bindings optional, bindings for SQL query
315: * @param array $remappings optional, an array of remapping arrays
316: * @param string $queryTemplate optional, query template
317: *
318: * @return array
319: */
320: public function findMulti( $types, $sql, $bindings = array(), $remappings = array(), $queryTemplate = ' %s.%s AS %s__%s' )
321: {
322: if ( !is_array( $types ) ) $types = explode( ',', $types );
323: if ( !is_array( $sql ) ) {
324: $writer = $this->toolbox->getWriter();
325: $adapter = $this->toolbox->getDatabaseAdapter();
326:
327: //Repair the query, replace book.* with book.id AS book_id etc..
328: foreach( $types as $type ) {
329: $pattern = " {$type}.*";
330: if ( strpos( $sql, $pattern ) !== FALSE ) {
331: $newSelectorArray = array();
332: $columns = $writer->getColumns( $type );
333: foreach( $columns as $column => $definition ) {
334: $newSelectorArray[] = sprintf( $queryTemplate, $type, $column, $type, $column );
335: }
336: $newSelector = implode( ',', $newSelectorArray );
337: $sql = str_replace( $pattern, $newSelector, $sql );
338: }
339: }
340:
341: $rows = $adapter->get( $sql, $bindings );
342: } else {
343: $rows = $sql;
344: }
345:
346: //Gather the bean data from the query results using the prefix
347: $wannaBeans = array();
348: foreach( $types as $type ) {
349: $wannaBeans[$type] = array();
350: $prefix = "{$type}__";
351: foreach( $rows as $rowkey=>$row ) {
352: $wannaBean = array();
353: foreach( $row as $cell => $value ) {
354: if ( strpos( $cell, $prefix ) === 0 ) {
355: $property = substr( $cell, strlen( $prefix ) );
356: unset( $rows[$rowkey][$cell] );
357: $wannaBean[$property] = $value;
358: }
359: }
360: if ( !isset( $wannaBean['id'] ) ) continue;
361: if ( is_null( $wannaBean['id'] ) ) continue;
362: $wannaBeans[$type][$wannaBean['id']] = $wannaBean;
363: }
364: }
365:
366: //Turn the rows into beans
367: $beans = array();
368: foreach( $wannaBeans as $type => $wannabees ) {
369: $beans[$type] = $this->redbean->convertToBeans( $type, $wannabees );
370: }
371:
372: //Apply additional re-mappings
373: foreach($remappings as $remapping) {
374: $a = $remapping['a'];
375: $b = $remapping['b'];
376: $matcher = $remapping['matcher'];
377: $do = $remapping['do'];
378: foreach( $beans[$a] as $bean ) {
379: foreach( $beans[$b] as $putBean ) {
380: if ( $matcher( $bean, $putBean, $beans ) ) $do( $bean, $putBean, $beans, $remapping );
381: }
382: }
383: }
384: return $beans;
385: }
386: }
387: