1: <?php
2:
3: namespace RedBeanPHP;
4:
5: use RedBeanPHP\Adapter\DBAdapter as DBAdapter;
6: use RedBeanPHP\QueryWriter as QueryWriter;
7: use RedBeanPHP\BeanHelper as BeanHelper;
8: use RedBeanPHP\RedException\SQL as SQLException;
9: use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter;
10: use RedBeanPHP\Cursor as Cursor;
11: use RedBeanPHP\Cursor\NullCursor as NullCursor;
12:
13: /**
14: * Abstract Repository.
15: *
16: * OODB manages two repositories, a fluid one that
17: * adjust the database schema on-the-fly to accomodate for
18: * new bean types (tables) and new properties (columns) and
19: * a frozen one for use in a production environment. OODB
20: * allows you to swap the repository instances using the freeze()
21: * method.
22: *
23: * @file RedBeanPHP/Repository.php
24: * @author Gabor de Mooij and the RedBeanPHP community
25: * @license BSD/GPLv2
26: *
27: * @copyright
28: * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community
29: * This source file is subject to the BSD/GPLv2 License that is bundled
30: * with this source code in the file license.txt.
31: */
32: abstract class Repository
33: {
34: /**
35: * @var array
36: */
37: protected $stash = NULL;
38:
39: /*
40: * @var integer
41: */
42: protected $nesting = 0;
43:
44: /**
45: * @var DBAdapter
46: */
47: protected $writer;
48:
49: /**
50: * Stores a bean and its lists in one run.
51: *
52: * @param OODBBean $bean bean to process
53: *
54: * @return void
55: */
56: protected function storeBeanWithLists( OODBBean $bean )
57: {
58: $sharedAdditions = $sharedTrashcan = $sharedresidue = $sharedItems = $ownAdditions = $ownTrashcan = $ownresidue = $embeddedBeans = array(); //Define groups
59: foreach ( $bean as $property => $value ) {
60: $value = ( $value instanceof SimpleModel ) ? $value->unbox() : $value;
61: if ( $value instanceof OODBBean ) {
62: $this->processEmbeddedBean( $embeddedBeans, $bean, $property, $value );
63: $bean->setMeta("sys.typeof.{$property}", $value->getMeta('type'));
64: } elseif ( is_array( $value ) ) {
65: $originals = $bean->moveMeta( 'sys.shadow.' . $property, array() );
66: if ( strpos( $property, 'own' ) === 0 ) {
67: list( $ownAdditions, $ownTrashcan, $ownresidue ) = $this->processGroups( $originals, $value, $ownAdditions, $ownTrashcan, $ownresidue );
68: $listName = lcfirst( substr( $property, 3 ) );
69: if ($bean->moveMeta( 'sys.exclusive-'. $listName ) ) {
70: OODBBean::setMetaAll( $ownTrashcan, 'sys.garbage', TRUE );
71: OODBBean::setMetaAll( $ownAdditions, 'sys.buildcommand.fkdependson', $bean->getMeta( 'type' ) );
72: }
73: unset( $bean->$property );
74: } elseif ( strpos( $property, 'shared' ) === 0 ) {
75: list( $sharedAdditions, $sharedTrashcan, $sharedresidue ) = $this->processGroups( $originals, $value, $sharedAdditions, $sharedTrashcan, $sharedresidue );
76: unset( $bean->$property );
77: }
78: }
79: }
80: $this->storeBean( $bean );
81: $this->processTrashcan( $bean, $ownTrashcan );
82: $this->processAdditions( $bean, $ownAdditions );
83: $this->processResidue( $ownresidue );
84: $this->processSharedTrashcan( $bean, $sharedTrashcan );
85: $this->processSharedAdditions( $bean, $sharedAdditions );
86: $this->processSharedResidue( $bean, $sharedresidue );
87: }
88:
89: /**
90: * Process groups. Internal function. Processes different kind of groups for
91: * storage function. Given a list of original beans and a list of current beans,
92: * this function calculates which beans remain in the list (residue), which
93: * have been deleted (are in the trashcan) and which beans have been added
94: * (additions).
95: *
96: * @param array $originals originals
97: * @param array $current the current beans
98: * @param array $additions beans that have been added
99: * @param array $trashcan beans that have been deleted
100: * @param array $residue beans that have been left untouched
101: *
102: * @return array
103: */
104: protected function processGroups( $originals, $current, $additions, $trashcan, $residue )
105: {
106: return array(
107: array_merge( $additions, array_diff( $current, $originals ) ),
108: array_merge( $trashcan, array_diff( $originals, $current ) ),
109: array_merge( $residue, array_intersect( $current, $originals ) )
110: );
111: }
112:
113: /**
114: * Processes an embedded bean.
115: *
116: * @param OODBBean|SimpleModel $embeddedBean the bean or model
117: *
118: * @return integer
119: */
120: protected function prepareEmbeddedBean( $embeddedBean )
121: {
122: if ( !$embeddedBean->id || $embeddedBean->getMeta( 'tainted' ) ) {
123: $this->store( $embeddedBean );
124: }
125:
126: return $embeddedBean->id;
127: }
128:
129: /**
130: * Processes a list of beans from a bean. A bean may contain lists. This
131: * method handles shared addition lists; i.e. the $bean->sharedObject properties.
132: *
133: * @param OODBBean $bean the bean
134: * @param array $sharedAdditions list with shared additions
135: *
136: * @return void
137: */
138: protected function processSharedAdditions( $bean, $sharedAdditions )
139: {
140: foreach ( $sharedAdditions as $addition ) {
141: if ( $addition instanceof OODBBean ) {
142: $this->oodb->getAssociationManager()->associate( $addition, $bean );
143: } else {
144: throw new RedException( 'Array may only contain OODBBeans' );
145: }
146: }
147: }
148:
149: /**
150: * Processes a list of beans from a bean. A bean may contain lists. This
151: * method handles own lists; i.e. the $bean->ownObject properties.
152: * A residue is a bean in an own-list that stays where it is. This method
153: * checks if there have been any modification to this bean, in that case
154: * the bean is stored once again, otherwise the bean will be left untouched.
155: *
156: * @param OODBBean $bean bean tor process
157: * @param array $ownresidue list to process
158: *
159: * @return void
160: */
161: protected function processResidue( $ownresidue )
162: {
163: foreach ( $ownresidue as $residue ) {
164: if ( $residue->getMeta( 'tainted' ) ) {
165: $this->store( $residue );
166: }
167: }
168: }
169:
170: /**
171: * Processes a list of beans from a bean. A bean may contain lists. This
172: * method handles own lists; i.e. the $bean->ownObject properties.
173: * A trash can bean is a bean in an own-list that has been removed
174: * (when checked with the shadow). This method
175: * checks if the bean is also in the dependency list. If it is the bean will be removed.
176: * If not, the connection between the bean and the owner bean will be broken by
177: * setting the ID to NULL.
178: *
179: * @param OODBBean $bean bean to process
180: * @param array $ownTrashcan list to process
181: *
182: * @return void
183: */
184: protected function processTrashcan( $bean, $ownTrashcan )
185: {
186: foreach ( $ownTrashcan as $trash ) {
187:
188: $myFieldLink = $bean->getMeta( 'type' ) . '_id';
189: $alias = $bean->getMeta( 'sys.alias.' . $trash->getMeta( 'type' ) );
190: if ( $alias ) $myFieldLink = $alias . '_id';
191:
192: if ( $trash->getMeta( 'sys.garbage' ) === true ) {
193: $this->trash( $trash );
194: } else {
195: $trash->$myFieldLink = NULL;
196: $this->store( $trash );
197: }
198: }
199: }
200:
201: /**
202: * Unassociates the list items in the trashcan.
203: *
204: * @param OODBBean $bean bean to process
205: * @param array $sharedTrashcan list to process
206: *
207: * @return void
208: */
209: protected function processSharedTrashcan( $bean, $sharedTrashcan )
210: {
211: foreach ( $sharedTrashcan as $trash ) {
212: $this->oodb->getAssociationManager()->unassociate( $trash, $bean );
213: }
214: }
215:
216: /**
217: * Stores all the beans in the residue group.
218: *
219: * @param OODBBean $bean bean to process
220: * @param array $sharedresidue list to process
221: *
222: * @return void
223: */
224: protected function processSharedResidue( $bean, $sharedresidue )
225: {
226: foreach ( $sharedresidue as $residue ) {
227: $this->store( $residue );
228: }
229: }
230:
231: /**
232: * Determines whether the bean has 'loaded lists' or
233: * 'loaded embedded beans' that need to be processed
234: * by the store() method.
235: *
236: * @param OODBBean $bean bean to be examined
237: *
238: * @return boolean
239: */
240: protected function hasListsOrObjects( OODBBean $bean )
241: {
242: $processLists = FALSE;
243: foreach ( $bean as $value ) {
244: if ( is_array( $value ) || is_object( $value ) ) {
245: $processLists = TRUE;
246: break;
247: }
248: }
249:
250: return $processLists;
251: }
252:
253: /**
254: * Converts an embedded bean to an ID, removed the bean property and
255: * stores the bean in the embedded beans array.
256: *
257: * @param array $embeddedBeans destination array for embedded bean
258: * @param OODBBean $bean target bean to process
259: * @param string $property property that contains the embedded bean
260: * @param OODBBean $value embedded bean itself
261: *
262: * @return void
263: */
264: protected function processEmbeddedBean( &$embeddedBeans, $bean, $property, OODBBean $value )
265: {
266: $linkField = $property . '_id';
267: $id = $this->prepareEmbeddedBean( $value );
268: if ($bean->$linkField != $id) $bean->$linkField = $id;
269: $bean->setMeta( 'cast.' . $linkField, 'id' );
270: $embeddedBeans[$linkField] = $value;
271: unset( $bean->$property );
272: }
273:
274: /**
275: * Constructor, requires a query writer.
276: * Creates a new instance of the bean respository class.
277: *
278: * @param QueryWriter $writer the Query Writer to use for this repository
279: *
280: * @return void
281: */
282: public function __construct( OODB $oodb, QueryWriter $writer )
283: {
284: $this->writer = $writer;
285: $this->oodb = $oodb;
286: }
287:
288: /**
289: * Checks whether a OODBBean bean is valid.
290: * If the type is not valid or the ID is not valid it will
291: * throw an exception: Security.
292: *
293: * @param OODBBean $bean the bean that needs to be checked
294: *
295: * @return void
296: */
297: public function check( OODBBean $bean )
298: {
299: //Is all meta information present?
300: if ( !isset( $bean->id ) ) {
301: throw new RedException( 'Bean has incomplete Meta Information id ' );
302: }
303: if ( !( $bean->getMeta( 'type' ) ) ) {
304: throw new RedException( 'Bean has incomplete Meta Information II' );
305: }
306: //Pattern of allowed characters
307: $pattern = '/[^a-z0-9_]/i';
308: //Does the type contain invalid characters?
309: if ( preg_match( $pattern, $bean->getMeta( 'type' ) ) ) {
310: throw new RedException( 'Bean Type is invalid' );
311: }
312: //Are the properties and values valid?
313: foreach ( $bean as $prop => $value ) {
314: if (
315: is_array( $value )
316: || ( is_object( $value ) )
317: ) {
318: throw new RedException( "Invalid Bean value: property $prop" );
319: } else if (
320: strlen( $prop ) < 1
321: || preg_match( $pattern, $prop )
322: ) {
323: throw new RedException( "Invalid Bean property: property $prop" );
324: }
325: }
326: }
327:
328: /**
329: * Searches the database for a bean that matches conditions $conditions and sql $addSQL
330: * and returns an array containing all the beans that have been found.
331: *
332: * Conditions need to take form:
333: *
334: * <code>
335: * array(
336: * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' )
337: * 'PROPERTY' => array( POSSIBLE VALUES... )
338: * );
339: * </code>
340: *
341: * All conditions are glued together using the AND-operator, while all value lists
342: * are glued using IN-operators thus acting as OR-conditions.
343: *
344: * Note that you can use property names; the columns will be extracted using the
345: * appropriate bean formatter.
346: *
347: * @param string $type type of beans you are looking for
348: * @param array $conditions list of conditions
349: * @param string $addSQL SQL to be used in query
350: * @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not)
351: *
352: * @return array
353: */
354: public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() )
355: {
356: //for backward compatibility, allow mismatch arguments:
357: if ( is_array( $sql ) ) {
358: if ( isset( $sql[1] ) ) {
359: $bindings = $sql[1];
360: }
361: $sql = $sql[0];
362: }
363: try {
364: $beans = $this->convertToBeans( $type, $this->writer->queryRecord( $type, $conditions, $sql, $bindings ) );
365:
366: return $beans;
367: } catch ( SQLException $exception ) {
368: $this->handleException( $exception );
369: }
370:
371: return array();
372: }
373:
374: /**
375: * Finds a BeanCollection.
376: *
377: * @param string $type type of beans you are looking for
378: * @param string $sql SQL to be used in query
379: * @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not)
380: *
381: * @return BeanCollection
382: */
383: public function findCollection( $type, $sql, $bindings = array() )
384: {
385: try {
386: $cursor = $this->writer->queryRecordWithCursor( $type, $sql, $bindings );
387: return new BeanCollection( $type, $this, $cursor );
388: } catch ( SQLException $exception ) {
389: $this->handleException( $exception );
390: }
391: return new BeanCollection( $type, $this, new NullCursor );
392: }
393:
394: /**
395: * Stores a bean in the database. This method takes a
396: * OODBBean Bean Object $bean and stores it
397: * in the database. If the database schema is not compatible
398: * with this bean and RedBean runs in fluid mode the schema
399: * will be altered to store the bean correctly.
400: * If the database schema is not compatible with this bean and
401: * RedBean runs in frozen mode it will throw an exception.
402: * This function returns the primary key ID of the inserted
403: * bean.
404: *
405: * The return value is an integer if possible. If it is not possible to
406: * represent the value as an integer a string will be returned. We use
407: * explicit casts instead of functions to preserve performance
408: * (0.13 vs 0.28 for 10000 iterations on Core i3).
409: *
410: * @param OODBBean|SimpleModel $bean bean to store
411: *
412: * @return integer|string
413: */
414: public function store( $bean )
415: {
416: $processLists = $this->hasListsOrObjects( $bean );
417: if ( !$processLists && !$bean->getMeta( 'tainted' ) ) {
418: return $bean->getID(); //bail out!
419: }
420: $this->oodb->signal( 'update', $bean );
421: $processLists = $this->hasListsOrObjects( $bean ); //check again, might have changed by model!
422: if ( $processLists ) {
423: $this->storeBeanWithLists( $bean );
424: } else {
425: $this->storeBean( $bean );
426: }
427: $this->oodb->signal( 'after_update', $bean );
428:
429: return ( (string) $bean->id === (string) (int) $bean->id ) ? (int) $bean->id : (string) $bean->id;
430: }
431:
432: /**
433: * Returns an array of beans. Pass a type and a series of ids and
434: * this method will bring you the corresponding beans.
435: *
436: * important note: Because this method loads beans using the load()
437: * function (but faster) it will return empty beans with ID 0 for
438: * every bean that could not be located. The resulting beans will have the
439: * passed IDs as their keys.
440: *
441: * @param string $type type of beans
442: * @param array $ids ids to load
443: *
444: * @return array
445: */
446: public function batch( $type, $ids )
447: {
448: if ( !$ids ) {
449: return array();
450: }
451: $collection = array();
452: try {
453: $rows = $this->writer->queryRecord( $type, array( 'id' => $ids ) );
454: } catch ( SQLException $e ) {
455: $this->handleException( $e );
456: $rows = FALSE;
457: }
458: $this->stash[$this->nesting] = array();
459: if ( !$rows ) {
460: return array();
461: }
462: foreach ( $rows as $row ) {
463: $this->stash[$this->nesting][$row['id']] = $row;
464: }
465: foreach ( $ids as $id ) {
466: $collection[$id] = $this->load( $type, $id );
467: }
468: $this->stash[$this->nesting] = NULL;
469:
470: return $collection;
471: }
472:
473: /**
474: * This is a convenience method; it converts database rows
475: * (arrays) into beans. Given a type and a set of rows this method
476: * will return an array of beans of the specified type loaded with
477: * the data fields provided by the result set from the database.
478: *
479: * New in 4.3.2: meta mask. The meta mask is a special mask to send
480: * data from raw result rows to the meta store of the bean. This is
481: * useful for bundling additional information with custom queries.
482: * Values of every column whos name starts with $mask will be
483: * transferred to the meta section of the bean under key 'data.bundle'.
484: *
485: * @param string $type type of beans you would like to have
486: * @param array $rows rows from the database result
487: * @param string $mask meta mask to apply (optional)
488: *
489: * @return array
490: */
491: public function convertToBeans( $type, $rows, $mask = NULL )
492: {
493: $masklen = 0;
494: if ( $mask !== NULL ) $masklen = mb_strlen( $mask );
495:
496: $collection = array();
497: $this->stash[$this->nesting] = array();
498: foreach ( $rows as $row ) {
499: $meta = array();
500: if ( !is_null( $mask ) ) {
501: foreach( $row as $key => $value ) {
502: if ( strpos( $key, $mask ) === 0 ) {
503: unset( $row[$key] );
504: $meta[$key] = $value;
505: }
506: }
507: }
508:
509: $id = $row['id'];
510: $this->stash[$this->nesting][$id] = $row;
511: $collection[$id] = $this->load( $type, $id );
512:
513: if ( $mask !== NULL ) {
514: $collection[$id]->setMeta( 'data.bundle', $meta );
515: }
516: }
517: $this->stash[$this->nesting] = NULL;
518:
519: return $collection;
520: }
521:
522: /**
523: * Counts the number of beans of type $type.
524: * This method accepts a second argument to modify the count-query.
525: * A third argument can be used to provide bindings for the SQL snippet.
526: *
527: * @param string $type type of bean we are looking for
528: * @param string $addSQL additional SQL snippet
529: * @param array $bindings parameters to bind to SQL
530: *
531: * @return integer
532: */
533: public function count( $type, $addSQL = '', $bindings = array() )
534: {
535: $type = AQueryWriter::camelsSnake( $type );
536: if ( count( explode( '_', $type ) ) > 2 ) {
537: throw new RedException( 'Invalid type for count.' );
538: }
539:
540: try {
541: return (int) $this->writer->queryRecordCount( $type, array(), $addSQL, $bindings );
542: } catch ( SQLException $exception ) {
543: if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array(
544: QueryWriter::C_SQLSTATE_NO_SUCH_TABLE,
545: QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ) ) ) {
546: throw $exception;
547: }
548: }
549:
550: return 0;
551: }
552:
553: /**
554: * Removes a bean from the database.
555: * This function will remove the specified OODBBean
556: * Bean Object from the database.
557: *
558: * @param OODBBean|SimpleModel $bean bean you want to remove from database
559: *
560: * @return void
561: */
562: public function trash( $bean )
563: {
564: $this->oodb->signal( 'delete', $bean );
565: foreach ( $bean as $property => $value ) {
566: if ( $value instanceof OODBBean ) {
567: unset( $bean->$property );
568: }
569: if ( is_array( $value ) ) {
570: if ( strpos( $property, 'own' ) === 0 ) {
571: unset( $bean->$property );
572: } elseif ( strpos( $property, 'shared' ) === 0 ) {
573: unset( $bean->$property );
574: }
575: }
576: }
577: try {
578: $this->writer->deleteRecord( $bean->getMeta( 'type' ), array( 'id' => array( $bean->id ) ), NULL );
579: } catch ( SQLException $exception ) {
580: $this->handleException( $exception );
581: }
582: $bean->id = 0;
583: $this->oodb->signal( 'after_delete', $bean );
584: }
585:
586: /**
587: * Checks whether the specified table already exists in the database.
588: * Not part of the Object Database interface!
589: *
590: * @deprecated Use AQueryWriter::typeExists() instead.
591: *
592: * @param string $table table name
593: *
594: * @return boolean
595: */
596: public function tableExists( $table )
597: {
598: return $this->writer->tableExists( $table );
599: }
600:
601: /**
602: * Trash all beans of a given type. Wipes an entire type of bean.
603: *
604: * @param string $type type of bean you wish to delete all instances of
605: *
606: * @return boolean
607: */
608: public function wipe( $type )
609: {
610: try {
611: $this->writer->wipe( $type );
612:
613: return TRUE;
614: } catch ( SQLException $exception ) {
615: if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ) ) ) {
616: throw $exception;
617: }
618:
619: return FALSE;
620: }
621: }
622: }
623: