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;
14 * Abstract Repository.
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()
23 * @file RedBeanPHP/Repository.php
24 * @author Gabor de Mooij and the RedBeanPHP community
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.
32 abstract class Repository
37 protected $stash = NULL;
42 protected $nesting = 0;
50 * Stores a bean and its lists in one run.
52 * @param OODBBean $bean bean to process
56 protected function storeBeanWithLists( OODBBean $bean )
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;
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' ) );
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 );
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 );
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
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
107 protected function processGroups( $originals, $current, $additions, $trashcan, $residue )
110 array_merge( $additions, array_diff( $current, $originals ) ),
111 array_merge( $trashcan, array_diff( $originals, $current ) ),
112 array_merge( $residue, array_intersect( $current, $originals ) )
117 * Processes an embedded bean.
119 * @param OODBBean|SimpleModel $embeddedBean the bean or model
123 protected function prepareEmbeddedBean( $embeddedBean )
125 if ( !$embeddedBean->id || $embeddedBean->getMeta( 'tainted' ) ) {
126 $this->store( $embeddedBean );
129 return $embeddedBean->id;
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.
136 * @param OODBBean $bean the bean
137 * @param array $sharedAdditions list with shared additions
141 protected function processSharedAdditions( $bean, $sharedAdditions )
143 foreach ( $sharedAdditions as $addition ) {
144 if ( $addition instanceof OODBBean ) {
145 $this->oodb->getAssociationManager()->associate( $addition, $bean );
147 throw new RedException( 'Array may only contain OODBBeans' );
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.
159 * @param OODBBean $bean bean tor process
160 * @param array $ownresidue list to process
164 protected function processResidue( $ownresidue )
166 foreach ( $ownresidue as $residue ) {
167 if ( $residue->getMeta( 'tainted' ) ) {
168 $this->store( $residue );
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.
182 * @param OODBBean $bean bean to process
183 * @param array $ownTrashcan list to process
187 protected function processTrashcan( $bean, $ownTrashcan )
189 foreach ( $ownTrashcan as $trash ) {
191 $myFieldLink = $bean->getMeta( 'type' ) . '_id';
192 $alias = $bean->getMeta( 'sys.alias.' . $trash->getMeta( 'type' ) );
193 if ( $alias ) $myFieldLink = $alias . '_id';
195 if ( $trash->getMeta( 'sys.garbage' ) === true ) {
196 $this->trash( $trash );
198 $trash->$myFieldLink = NULL;
199 $this->store( $trash );
205 * Unassociates the list items in the trashcan.
207 * @param OODBBean $bean bean to process
208 * @param array $sharedTrashcan list to process
212 protected function processSharedTrashcan( $bean, $sharedTrashcan )
214 foreach ( $sharedTrashcan as $trash ) {
215 $this->oodb->getAssociationManager()->unassociate( $trash, $bean );
220 * Stores all the beans in the residue group.
222 * @param OODBBean $bean bean to process
223 * @param array $sharedresidue list to process
227 protected function processSharedResidue( $bean, $sharedresidue )
229 foreach ( $sharedresidue as $residue ) {
230 $this->store( $residue );
235 * Determines whether the bean has 'loaded lists' or
236 * 'loaded embedded beans' that need to be processed
237 * by the store() method.
239 * @param OODBBean $bean bean to be examined
243 protected function hasListsOrObjects( OODBBean $bean )
245 $processLists = FALSE;
246 foreach ( $bean as $value ) {
247 if ( is_array( $value ) || is_object( $value ) ) {
248 $processLists = TRUE;
253 return $processLists;
257 * Converts an embedded bean to an ID, removed the bean property and
258 * stores the bean in the embedded beans array.
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
267 protected function processEmbeddedBean( &$embeddedBeans, $bean, $property, OODBBean $value )
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 );
278 * Constructor, requires a query writer.
279 * Creates a new instance of the bean respository class.
281 * @param QueryWriter $writer the Query Writer to use for this repository
285 public function __construct( OODB $oodb, QueryWriter $writer )
287 $this->writer = $writer;
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.
296 * @param OODBBean $bean the bean that needs to be checked
300 public function check( OODBBean $bean )
302 //Is all meta information present?
303 if ( !isset( $bean->id ) ) {
304 throw new RedException( 'Bean has incomplete Meta Information id ' );
306 if ( !( $bean->getMeta( 'type' ) ) ) {
307 throw new RedException( 'Bean has incomplete Meta Information II' );
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' );
315 //Are the properties and values valid?
316 foreach ( $bean as $prop => $value ) {
319 || ( is_object( $value ) )
321 throw new RedException( "Invalid Bean value: property $prop" );
324 || preg_match( $pattern, $prop )
326 throw new RedException( "Invalid Bean property: property $prop" );
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.
335 * Conditions need to take form:
339 * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' )
340 * 'PROPERTY' => array( POSSIBLE VALUES... )
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.
347 * Note that you can use property names; the columns will be extracted using the
348 * appropriate bean formatter.
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)
357 public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() )
359 //for backward compatibility, allow mismatch arguments:
360 if ( is_array( $sql ) ) {
361 if ( isset( $sql[1] ) ) {
367 $beans = $this->convertToBeans( $type, $this->writer->queryRecord( $type, $conditions, $sql, $bindings ) );
370 } catch ( SQLException $exception ) {
371 $this->handleException( $exception );
378 * Finds a BeanCollection.
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)
384 * @return BeanCollection
386 public function findCollection( $type, $sql, $bindings = array() )
389 $cursor = $this->writer->queryRecordWithCursor( $type, $sql, $bindings );
390 return new BeanCollection( $type, $this, $cursor );
391 } catch ( SQLException $exception ) {
392 $this->handleException( $exception );
394 return new BeanCollection( $type, $this, new NullCursor );
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
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).
413 * @param OODBBean|SimpleModel $bean bean to store
415 * @return integer|string
417 public function store( $bean )
419 $processLists = $this->hasListsOrObjects( $bean );
420 if ( !$processLists && !$bean->getMeta( 'tainted' ) ) {
421 return $bean->getID(); //bail out!
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 );
428 $this->storeBean( $bean );
430 $this->oodb->signal( 'after_update', $bean );
432 return ( (string) $bean->id === (string) (int) $bean->id ) ? (int) $bean->id : (string) $bean->id;
436 * Returns an array of beans. Pass a type and a series of ids and
437 * this method will bring you the corresponding beans.
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.
444 * @param string $type type of beans
445 * @param array $ids ids to load
449 public function batch( $type, $ids )
454 $collection = array();
456 $rows = $this->writer->queryRecord( $type, array( 'id' => $ids ) );
457 } catch ( SQLException $e ) {
458 $this->handleException( $e );
461 $this->stash[$this->nesting] = array();
465 foreach ( $rows as $row ) {
466 $this->stash[$this->nesting][$row['id']] = $row;
468 foreach ( $ids as $id ) {
469 $collection[$id] = $this->load( $type, $id );
471 $this->stash[$this->nesting] = NULL;
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.
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'.
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)
494 public function convertToBeans( $type, $rows, $mask = NULL )
497 if ( $mask !== NULL ) $masklen = mb_strlen( $mask );
499 $collection = array();
500 $this->stash[$this->nesting] = array();
501 foreach ( $rows as $row ) {
503 if ( !is_null( $mask ) ) {
504 foreach( $row as $key => $value ) {
505 if ( strpos( $key, $mask ) === 0 ) {
507 $meta[$key] = $value;
513 $this->stash[$this->nesting][$id] = $row;
514 $collection[$id] = $this->load( $type, $id );
516 if ( $mask !== NULL ) {
517 $collection[$id]->setMeta( 'data.bundle', $meta );
520 $this->stash[$this->nesting] = NULL;
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.
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
536 public function count( $type, $addSQL = '', $bindings = array() )
538 $type = AQueryWriter::camelsSnake( $type );
539 if ( count( explode( '_', $type ) ) > 2 ) {
540 throw new RedException( 'Invalid type for count.' );
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 ) ) ) {
557 * Removes a bean from the database.
558 * This function will remove the specified OODBBean
559 * Bean Object from the database.
561 * @param OODBBean|SimpleModel $bean bean you want to remove from database
565 public function trash( $bean )
567 $this->oodb->signal( 'delete', $bean );
568 foreach ( $bean as $property => $value ) {
569 if ( $value instanceof OODBBean ) {
570 unset( $bean->$property );
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 );
581 $this->writer->deleteRecord( $bean->getMeta( 'type' ), array( 'id' => array( $bean->id ) ), NULL );
582 } catch ( SQLException $exception ) {
583 $this->handleException( $exception );
586 $this->oodb->signal( 'after_delete', $bean );
590 * Checks whether the specified table already exists in the database.
591 * Not part of the Object Database interface!
593 * @deprecated Use AQueryWriter::typeExists() instead.
595 * @param string $table table name
599 public function tableExists( $table )
601 return $this->writer->tableExists( $table );
605 * Trash all beans of a given type. Wipes an entire type of bean.
607 * @param string $type type of bean you wish to delete all instances of
611 public function wipe( $type )
614 $this->writer->wipe( $type );
617 } catch ( SQLException $exception ) {
618 if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ) ) ) {