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