5 use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter;
6 use RedBeanPHP\BeanHelper as BeanHelper;
7 use RedBeanPHP\RedException as RedException;
9 /* PHP 5.3 compatibility */
10 if (interface_exists('\JsonSerializable')) {
11 /* We extend JsonSerializable to avoid namespace conflicts,
12 can't define interface with special namespace in PHP */
13 interface Jsonable extends \JsonSerializable {};
15 interface Jsonable {};
19 * OODBBean (Object Oriented DataBase Bean).
21 * to exchange information with the database. A bean represents
22 * a single table row and offers generic services for interaction
23 * with databases systems as well as some meta-data.
25 * @file RedBeanPHP/OODBBean.php
26 * @author Gabor de Mooij and the RedBeanPHP community
28 * @desc OODBBean represents a bean. RedBeanPHP uses beans
31 * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community.
32 * This source file is subject to the BSD/GPLv2 License that is bundled
33 * with this source code in the file license.txt.
35 class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable,Jsonable
40 const C_ERR_IGNORE = FALSE;
42 const C_ERR_NOTICE = 2;
44 const C_ERR_EXCEPTION = 4;
46 const C_ERR_FATAL = 6;
51 protected static $errorHandlingFUSE = FALSE;
56 protected static $errorHandler = NULL;
61 protected static $aliases = array();
66 protected static $autoResolve = FALSE;
69 * This is where the real properties of the bean live. They are stored and retrieved
70 * by the magic getter and setter (__get and __set).
72 * @var array $properties
74 protected $properties = array();
77 * Here we keep the meta data of a bean.
81 protected $__info = array();
84 * The BeanHelper allows the bean to access the toolbox objects to implement
85 * rich functionality, otherwise you would have to do everything with R or
90 protected $beanHelper = NULL;
95 protected $fetchType = NULL;
100 protected $withSql = '';
105 protected $withParams = array();
110 protected $aliasName = NULL;
115 protected $via = NULL;
120 protected $noLoad = FALSE;
125 protected $all = FALSE;
128 * Sets the error mode for FUSE.
129 * What to do if a FUSE model method does not exist?
130 * You can set the following options:
132 * * OODBBean::C_ERR_IGNORE (default), ignores the call, returns NULL
133 * * OODBBean::C_ERR_LOG, logs the incident using error_log
134 * * OODBBean::C_ERR_NOTICE, triggers a E_USER_NOTICE
135 * * OODBBean::C_ERR_WARN, triggers a E_USER_WARNING
136 * * OODBBean::C_ERR_EXCEPTION, throws an exception
137 * * OODBBean::C_ERR_FUNC, allows you to specify a custom handler (function)
138 * * OODBBean::C_ERR_FATAL, triggers a E_USER_ERROR
141 * Custom handler method signature: handler( array (
142 * 'message' => string
148 * This method returns the old mode and handler as an array.
150 * @param integer $mode error handling mode
151 * @param callable|NULL $func custom handler
155 public static function setErrorHandlingFUSE($mode, $func = NULL) {
157 $mode !== self::C_ERR_IGNORE
158 && $mode !== self::C_ERR_LOG
159 && $mode !== self::C_ERR_NOTICE
160 && $mode !== self::C_ERR_WARN
161 && $mode !== self::C_ERR_EXCEPTION
162 && $mode !== self::C_ERR_FUNC
163 && $mode !== self::C_ERR_FATAL
164 ) throw new \Exception( 'Invalid error mode selected' );
166 if ( $mode === self::C_ERR_FUNC && !is_callable( $func ) ) {
167 throw new \Exception( 'Invalid error handler' );
170 $old = array( self::$errorHandlingFUSE, self::$errorHandler );
171 self::$errorHandlingFUSE = $mode;
172 if ( is_callable( $func ) ) {
173 self::$errorHandler = $func;
175 self::$errorHandler = NULL;
181 * Sets global aliases.
182 * Registers a batch of aliases in one go. This works the same as
183 * fetchAs and setAutoResolve but explicitly. For instance if you register
184 * the alias 'cover' for 'page' a property containing a reference to a
185 * page bean called 'cover' will correctly return the page bean and not
186 * a (non-existant) cover bean.
189 * R::aliases( array( 'cover' => 'page' ) );
190 * $book = R::dispense( 'book' );
191 * $page = R::dispense( 'page' );
192 * $book->cover = $page;
194 * $book = $book->fresh();
195 * $cover = $book->cover;
196 * echo $cover->getMeta( 'type' ); //page
199 * The format of the aliases registration array is:
201 * {alias} => {actual type}
203 * In the example above we use:
207 * From that point on, every bean reference to a cover
208 * will return a 'page' bean. Note that with autoResolve this
209 * feature along with fetchAs() is no longer very important, although
210 * relying on explicit aliases can be a bit faster.
212 * @param array $list list of global aliases to use
216 public static function aliases( $list )
218 self::$aliases = $list;
222 * Enables or disables auto-resolving fetch types.
223 * Auto-resolving aliased parent beans is convenient but can
224 * be slower and can create infinite recursion if you
225 * used aliases to break cyclic relations in your domain.
227 * @param boolean $automatic TRUE to enable automatic resolving aliased parents
231 public static function setAutoResolve( $automatic = TRUE )
233 self::$autoResolve = (boolean) $automatic;
237 * Sets a meta property for all beans. This is a quicker way to set
238 * the meta properties for a collection of beans because this method
239 * can directly access the property arrays of the beans.
240 * This method returns the beans.
242 * @param array $beans beans to set the meta property of
243 * @param string $property property to set
244 * @param mixed $value value
248 public static function setMetaAll( $beans, $property, $value )
250 foreach( $beans as $bean ) {
251 if ( $bean instanceof OODBBean ) $bean->__info[ $property ] = $value;
258 * Parses the join in the with-snippet.
263 * ->withCondition(' @joined.detail.title LIKE ? ')
267 * will automatically join 'detail' on book to
268 * access the title field.
270 * @note this feature requires Narrow Field Mode and Join Feature
271 * to be both activated (default).
273 * @param string $type the source type for the join
277 private function parseJoin( $type )
281 if ( strpos($this->withSql, '@joined.' ) !== FALSE ) {
282 $writer = $this->beanHelper->getToolBox()->getWriter();
283 $oldParts = $parts = explode( '@joined.', $this->withSql );
284 array_shift( $parts );
285 foreach($parts as $part) {
286 $explosion = explode( '.', $part );
287 $joinInfo = array_shift( $explosion );
288 //Dont join more than once..
289 if ( !isset( $joins[$joinInfo] ) ) {
290 $joins[ $joinInfo ] = true;
291 $joinSql .= $writer->writeJoin( $type, $joinInfo, 'LEFT' );
294 $this->withSql = implode( '', $oldParts );
295 $joinSql .= ' WHERE ';
302 * Obtains a shared list for a certain type.
304 * @param string $type the name of the list you want to retrieve.
308 private function getSharedList( $type, $redbean, $toolbox )
310 $writer = $toolbox->getWriter();
313 $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) );
314 if ( $oldName !== $this->via ) {
315 //set the new renaming rule
316 $writer->renameAssocTable( $oldName, $this->via );
322 if ($this->getID()) {
323 $type = $this->beau( $type );
324 $assocManager = $redbean->getAssociationManager();
325 $beans = $assocManager->related( $this, $type, $this->withSql, $this->withParams );
329 $this->withParams = array();
336 * Obtains the own list of a certain type.
338 * @param string $type name of the list you want to retrieve
339 * @param OODB $oodb The RB OODB object database instance
343 private function getOwnList( $type, $redbean )
345 $type = $this->beau( $type );
347 if ( $this->aliasName ) {
348 $parentField = $this->aliasName;
349 $myFieldLink = $parentField . '_id';
351 $this->__info['sys.alias.' . $type] = $this->aliasName;
353 $this->aliasName = NULL;
355 $parentField = $this->__info['type'];
356 $myFieldLink = $parentField . '_id';
361 if ( $this->getID() ) {
364 if ( count( $this->withParams ) > 0 ) {
365 reset( $this->withParams );
367 $firstKey = key( $this->withParams );
370 $joinSql = $this->parseJoin( $type );
372 if ( !is_numeric( $firstKey ) || $firstKey === NULL ) {
373 $bindings = $this->withParams;
374 $bindings[':slot0'] = $this->getID();
376 $beans = $redbean->find( $type, array(), " {$joinSql} $myFieldLink = :slot0 " . $this->withSql, $bindings );
378 $bindings = array_merge( array( $this->getID() ), $this->withParams );
380 $beans = $redbean->find( $type, array(), " {$joinSql} $myFieldLink = ? " . $this->withSql, $bindings );
385 $this->withParams = array();
387 foreach ( $beans as $beanFromList ) {
388 $beanFromList->__info['sys.parentcache.' . $parentField] = $this;
395 * Initializes a bean. Used by OODB for dispensing beans.
396 * It is not recommended to use this method to initialize beans. Instead
397 * use the OODB object to dispense new beans. You can use this method
398 * if you build your own bean dispensing mechanism.
400 * @param string $type type of the new bean
401 * @param BeanHelper $beanhelper bean helper to obtain a toolbox and a model
405 public function initializeForDispense( $type, BeanHelper $beanhelper )
407 $this->beanHelper = $beanhelper;
408 $this->__info['type'] = $type;
409 $this->__info['sys.id'] = 'id';
410 $this->__info['sys.orig'] = array( 'id' => 0 );
411 $this->__info['tainted'] = TRUE;
412 $this->__info['changed'] = TRUE;
413 $this->properties['id'] = 0;
417 * Sets the Bean Helper. Normally the Bean Helper is set by OODB.
418 * Here you can change the Bean Helper. The Bean Helper is an object
419 * providing access to a toolbox for the bean necessary to retrieve
420 * nested beans (bean lists: ownBean, sharedBean) without the need to
421 * rely on static calls to the facade (or make this class dep. on OODB).
423 * @param BeanHelper $helper helper to use for this bean
427 public function setBeanHelper( BeanHelper $helper )
429 $this->beanHelper = $helper;
433 * Returns an ArrayIterator so you can treat the bean like
434 * an array with the properties container as its contents.
435 * This method is meant for PHP and allows you to access beans as if
436 * they were arrays, i.e. using array notation:
438 * $bean[$key] = $value;
440 * Note that not all PHP functions work with the array interface.
442 * @return ArrayIterator
444 public function getIterator()
446 return new \ArrayIterator( $this->properties );
450 * Imports all values from an associative array $array. Chainable.
451 * This method imports the values in the first argument as bean
452 * propery and value pairs. Use the second parameter to provide a
453 * selection. If a selection array is passed, only the entries
454 * having keys mentioned in the selection array will be imported.
455 * Set the third parameter to TRUE to preserve spaces in selection keys.
457 * @param array $array what you want to import
458 * @param string|array $selection selection of values
459 * @param boolean $notrim if TRUE selection keys will NOT be trimmed
463 public function import( $array, $selection = FALSE, $notrim = FALSE )
465 if ( is_string( $selection ) ) {
466 $selection = explode( ',', $selection );
469 if ( !$notrim && is_array( $selection ) ) {
470 foreach ( $selection as $key => $selected ) {
471 $selection[$key] = trim( $selected );
475 foreach ( $array as $key => $value ) {
476 if ( $key != '__info' ) {
477 if ( !$selection || ( $selection && in_array( $key, $selection ) ) ) {
478 if ( is_array($value ) ) {
479 if ( isset( $value['_type'] ) ) {
480 $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $value['_type'] );
481 unset( $value['_type'] );
482 $bean->import($value);
485 $listBeans = array();
486 foreach( $value as $listKey => $listItem ) {
487 $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $listItem['_type'] );
488 unset( $listItem['_type'] );
489 $bean->import($listItem);
490 $list = &$this->$key;
491 $list[ $listKey ] = $bean;
495 $this->$key = $value;
505 * Fast way to import a row.
506 * Does not perform any checks.
508 * @param array $row a database row
512 public function importRow( $row )
514 $this->properties = $row;
515 $this->__info['sys.orig'] = $row;
516 $this->__info['changed'] = FALSE;
521 * Imports data from another bean. Chainable.
522 * Copies the properties from the source bean to the internal
525 * @param OODBBean $sourceBean the source bean to take properties from
529 public function importFrom( OODBBean $sourceBean )
531 $this->__info['tainted'] = TRUE;
532 $this->__info['changed'] = TRUE;
533 $this->properties = $sourceBean->properties;
539 * Injects the properties of another bean but keeps the original ID.
540 * Just like import() but keeps the original ID.
543 * @param OODBBean $otherBean the bean whose properties you would like to copy
547 public function inject( OODBBean $otherBean )
549 $myID = $this->properties['id'];
551 $this->import( $otherBean->export() );
559 * Exports the bean as an array.
560 * This function exports the contents of a bean to an array and returns
561 * the resulting array.
563 * @param boolean $meta set to TRUE if you want to export meta data as well
564 * @param boolean $parents set to TRUE if you want to export parents as well
565 * @param boolean $onlyMe set to TRUE if you want to export only this bean
566 * @param array $filters optional whitelist for export
570 public function export( $meta = FALSE, $parents = FALSE, $onlyMe = FALSE, $filters = array() )
575 foreach ( $this as $key => $value ) {
576 if ( substr( $key, -3 ) != '_id' ) continue;
578 $prop = substr( $key, 0, strlen( $key ) - 3 );
583 $hasFilters = is_array( $filters ) && count( $filters );
585 foreach ( $this as $key => $value ) {
586 if ( !$onlyMe && is_array( $value ) ) {
589 foreach ( $value as $i => $b ) {
590 if ( !( $b instanceof OODBBean ) ) continue;
591 $vn[] = $b->export( $meta, FALSE, FALSE, $filters );
594 } elseif ( $value instanceof OODBBean ) {
596 if ( !in_array( strtolower( $value->getMeta( 'type' ) ), $filters ) ) continue;
599 $value = $value->export( $meta, $parents, FALSE, $filters );
606 $arr['__info'] = $this->__info;
613 * Implements isset() function for use as an array.
615 * @param string $property name of the property you want to check
619 public function __isset( $property )
621 $property = $this->beau( $property );
623 if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) {
624 $property = substr($property, 1);
626 return isset( $this->properties[$property] );
630 * Returns the ID of the bean no matter what the ID field is.
632 * @return string|null
634 public function getID()
636 return ( isset( $this->properties['id'] ) ) ? (string) $this->properties['id'] : NULL;
640 * Unsets a property of a bean.
641 * Magic method, gets called implicitly when performing the unset() operation
642 * on a bean property.
644 * @param string $property property to unset
648 public function __unset( $property )
650 $property = $this->beau( $property );
652 if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) {
653 $property = substr($property, 1);
656 unset( $this->properties[$property] );
658 $shadowKey = 'sys.shadow.'.$property;
659 if ( isset( $this->__info[ $shadowKey ] ) ) unset( $this->__info[$shadowKey] );
661 //also clear modifiers
663 $this->withParams = array();
664 $this->aliasName = NULL;
665 $this->fetchType = NULL;
666 $this->noLoad = FALSE;
674 * Adds WHERE clause conditions to ownList retrieval.
675 * For instance to get the pages that belong to a book you would
676 * issue the following command: $book->ownPage
677 * However, to order these pages by number use:
680 * $book->with(' ORDER BY `number` ASC ')->ownPage
683 * the additional SQL snippet will be merged into the final
686 * @param string $sql SQL to be added to retrieval query.
687 * @param array $bindings array with parameters to bind to SQL snippet
691 public function with( $sql, $bindings = array() )
693 $this->withSql = $sql;
694 $this->withParams = $bindings;
699 * Just like with(). Except that this method prepends the SQL query snippet
700 * with AND which makes it slightly more comfortable to use a conditional
701 * SQL snippet. For instance to filter an own-list with pages (belonging to
702 * a book) on specific chapters you can use:
704 * $book->withCondition(' chapter = 3 ')->ownPage
706 * This will return in the own list only the pages having 'chapter == 3'.
708 * @param string $sql SQL to be added to retrieval query (prefixed by AND)
709 * @param array $bindings array with parameters to bind to SQL snippet
713 public function withCondition( $sql, $bindings = array() )
715 $this->withSql = ' AND ' . $sql;
716 $this->withParams = $bindings;
721 * Tells the bean to (re)load the following list without any
722 * conditions. If you have an ownList or sharedList with a
723 * condition you can use this method to reload the entire list.
728 * $bean->with( ' LIMIT 3 ' )->ownPage; //Just 3
729 * $bean->all()->ownPage; //Reload all pages
734 public function all()
741 * Tells the bean to only access the list but not load
742 * its contents. Use this if you only want to add something to a list
743 * and you have no interest in retrieving its contents from the database.
747 public function noLoad()
749 $this->noLoad = TRUE;
754 * Prepares an own-list to use an alias. This is best explained using
755 * an example. Imagine a project and a person. The project always involves
756 * two persons: a teacher and a student. The person beans have been aliased in this
757 * case, so to the project has a teacher_id pointing to a person, and a student_id
758 * also pointing to a person. Given a project, we obtain the teacher like this:
761 * $project->fetchAs('person')->teacher;
764 * Now, if we want all projects of a teacher we cant say:
767 * $teacher->ownProject
770 * because the $teacher is a bean of type 'person' and no project has been
771 * assigned to a person. Instead we use the alias() method like this:
774 * $teacher->alias('teacher')->ownProject
777 * now we get the projects associated with the person bean aliased as
780 * @param string $aliasName the alias name to use
784 public function alias( $aliasName )
786 $this->aliasName = $this->beau( $aliasName );
792 * Returns properties of bean as an array.
793 * This method returns the raw internal property list of the
794 * bean. Only use this method for optimization purposes. Otherwise
795 * use the export() method to export bean data to arrays.
799 public function getProperties()
801 return $this->properties;
805 * Returns properties of bean as an array.
806 * This method returns the raw internal property list of the
807 * bean. Only use this method for optimization purposes. Otherwise
808 * use the export() method to export bean data to arrays.
809 * This method returns an array with the properties array and
814 public function getPropertiesAndType()
816 return array( $this->properties, $this->__info['type'] );
820 * Turns a camelcase property name into an underscored property name.
824 * * oneACLRoute -> one_acl_route
825 * * camelCase -> camel_case
827 * Also caches the result to improve performance.
829 * @param string $property property to un-beautify
833 public function beau( $property )
835 static $beautifulColumns = array();
837 if ( ctype_lower( $property ) ) return $property;
840 ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) )
841 || ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) )
842 || ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) )
845 $property = preg_replace( '/List$/', '', $property );
849 if ( !isset( $beautifulColumns[$property] ) ) {
850 $beautifulColumns[$property] = AQueryWriter::camelsSnake( $property );
853 return $beautifulColumns[$property];
857 * Clears all modifiers.
861 public function clearModifiers()
864 $this->withParams = array();
865 $this->aliasName = NULL;
866 $this->fetchType = NULL;
867 $this->noLoad = FALSE;
874 * Determines whether a list is opened in exclusive mode or not.
875 * If a list has been opened in exclusive mode this method will return TRUE,
876 * othwerwise it will return FALSE.
878 * @param string $listName name of the list to check
882 public function isListInExclusiveMode( $listName )
884 $listName = $this->beau( $listName );
886 if ( strpos( $listName, 'xown' ) === 0 && ctype_upper( substr( $listName, 4, 1 ) ) ) {
887 $listName = substr($listName, 1);
890 $listName = lcfirst( substr( $listName, 3 ) );
892 return ( isset( $this->__info['sys.exclusive-'.$listName] ) && $this->__info['sys.exclusive-'.$listName] );
896 * Magic Getter. Gets the value for a specific property in the bean.
897 * If the property does not exist this getter will make sure no error
898 * occurs. This is because RedBean allows you to query (probe) for
899 * properties. If the property can not be found this method will
900 * return NULL instead.
902 * @param string $property name of the property you wish to obtain the value of
906 public function &__get( $property )
912 if ( !ctype_lower( $property ) ) {
913 $property = $this->beau( $property );
914 if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) {
915 $property = substr($property, 1);
916 $listName = lcfirst( substr( $property, 3 ) );
919 $this->__info['sys.exclusive-'.$listName] = TRUE;
920 } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) {
922 $listName = lcfirst( substr( $property, 3 ) );
923 } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) {
928 $fieldLink = $property . '_id';
929 $exists = isset( $this->properties[$property] );
931 //If not exists and no field link and no list, bail out.
932 if ( !$exists && !isset($this->$fieldLink) && (!$isOwn && !$isShared )) {
935 $this->withParams = array();
936 $this->aliasName = NULL;
937 $this->fetchType = NULL;
938 $this->noLoad = FALSE;
946 $hasAlias = (!is_null($this->aliasName));
947 $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ?
948 ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE;
949 $hasSQL = ($this->withSql !== '' || $this->via !== NULL);
950 $hasAll = (boolean) ($this->all);
952 //If exists and no list or exits and list not changed, bail out.
953 if ( $exists && ((!$isOwn && !$isShared ) || (!$hasSQL && !$differentAlias && !$hasAll)) ) {
956 $this->withParams = array();
957 $this->aliasName = NULL;
958 $this->fetchType = NULL;
959 $this->noLoad = FALSE;
962 return $this->properties[$property];
965 list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox();
967 if ( isset( $this->$fieldLink ) ) {
968 $this->__info['tainted'] = TRUE;
970 if ( isset( $this->__info["sys.parentcache.$property"] ) ) {
971 $bean = $this->__info["sys.parentcache.$property"];
973 if ( isset( self::$aliases[$property] ) ) {
974 $type = self::$aliases[$property];
975 } elseif ( $this->fetchType ) {
976 $type = $this->fetchType;
977 $this->fetchType = NULL;
982 if ( !is_null( $this->properties[$fieldLink] ) ) {
983 $bean = $redbean->load( $type, $this->properties[$fieldLink] );
984 //If the IDs dont match, we failed to load, so try autoresolv in that case...
985 if ( $bean->id !== $this->properties[$fieldLink] && self::$autoResolve ) {
986 $type = $this->beanHelper->getToolbox()->getWriter()->inferFetchType( $this->__info['type'], $property );
987 if ( !is_null( $type) ) {
988 $bean = $redbean->load( $type, $this->properties[$fieldLink] );
989 $this->__info["sys.autoresolved.{$property}"] = $type;
995 $this->properties[$property] = $bean;
997 $this->withParams = array();
998 $this->aliasName = NULL;
999 $this->fetchType = NULL;
1000 $this->noLoad = FALSE;
1004 return $this->properties[$property];
1007 //Implicit: elseif ( $isOwn || $isShared ) {
1008 if ( $this->noLoad ) {
1010 } elseif ( $isOwn ) {
1011 $beans = $this->getOwnList( $listName, $redbean );
1013 $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox );
1016 $this->properties[$property] = $beans;
1017 $this->__info["sys.shadow.$property"] = $beans;
1018 $this->__info['tainted'] = TRUE;
1020 $this->withSql = '';
1021 $this->withParams = array();
1022 $this->aliasName = NULL;
1023 $this->fetchType = NULL;
1024 $this->noLoad = FALSE;
1028 return $this->properties[$property];
1032 * Magic Setter. Sets the value for a specific property.
1033 * This setter acts as a hook for OODB to mark beans as tainted.
1034 * The tainted meta property can be retrieved using getMeta("tainted").
1035 * The tainted meta property indicates whether a bean has been modified and
1036 * can be used in various caching mechanisms.
1038 * @param string $property name of the property you wish to assign a value to
1039 * @param mixed $value the value you want to assign
1043 public function __set( $property, $value )
1049 if ( !ctype_lower( $property ) ) {
1050 $property = $this->beau( $property );
1051 if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) {
1052 $property = substr($property, 1);
1053 $listName = lcfirst( substr( $property, 3 ) );
1056 $this->__info['sys.exclusive-'.$listName] = TRUE;
1057 } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) {
1059 $listName = lcfirst( substr( $property, 3 ) );
1060 } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) {
1065 $hasAlias = (!is_null($this->aliasName));
1066 $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ?
1067 ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE;
1068 $hasSQL = ($this->withSql !== '' || $this->via !== NULL);
1069 $exists = isset( $this->properties[$property] );
1070 $fieldLink = $property . '_id';
1072 if ( ($isOwn || $isShared) && (!$exists || $hasSQL || $differentAlias) ) {
1074 if ( !$this->noLoad ) {
1075 list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox();
1077 $beans = $this->getOwnList( $listName, $redbean );
1079 $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox );
1081 $this->__info["sys.shadow.$property"] = $beans;
1085 $this->withSql = '';
1086 $this->withParams = array();
1087 $this->aliasName = NULL;
1088 $this->fetchType = NULL;
1089 $this->noLoad = FALSE;
1093 $this->__info['tainted'] = TRUE;
1094 $this->__info['changed'] = TRUE;
1096 if ( array_key_exists( $fieldLink, $this->properties ) && !( $value instanceof OODBBean ) ) {
1097 if ( is_null( $value ) || $value === FALSE ) {
1099 unset( $this->properties[ $property ]);
1100 $this->properties[ $fieldLink ] = NULL;
1104 throw new RedException( 'Cannot cast to bean.' );
1108 if ( $value === FALSE ) {
1110 } elseif ( $value === TRUE ) {
1112 } elseif ( $value instanceof \DateTime ) {
1113 $value = $value->format( 'Y-m-d H:i:s' );
1116 $this->properties[$property] = $value;
1120 * Sets a property directly, for internal use only.
1122 * @param string $property property
1123 * @param mixed $value value
1124 * @param boolean $updateShadow whether you want to update the shadow
1125 * @param boolean $taint whether you want to mark the bean as tainted
1129 public function setProperty( $property, $value, $updateShadow = FALSE, $taint = FALSE )
1131 $this->properties[$property] = $value;
1133 if ( $updateShadow ) {
1134 $this->__info['sys.shadow.' . $property] = $value;
1138 $this->__info['tainted'] = TRUE;
1139 $this->__info['changed'] = TRUE;
1144 * Returns the value of a meta property. A meta property
1145 * contains additional information about the bean object that will not
1146 * be stored in the database. Meta information is used to instruct
1147 * RedBeanPHP as well as other systems how to deal with the bean.
1148 * If the property cannot be found this getter will return NULL instead.
1153 * $bean->setMeta( 'flush-cache', TRUE );
1156 * RedBeanPHP also stores meta data in beans, this meta data uses
1157 * keys prefixed with 'sys.' (system).
1159 * @param string $path path to property in meta data
1160 * @param mixed $default default value
1164 public function getMeta( $path, $default = NULL )
1166 return ( isset( $this->__info[$path] ) ) ? $this->__info[$path] : $default;
1170 * Gets and unsets a meta property.
1171 * Moves a meta property out of the bean.
1172 * This is a short-cut method that can be used instead
1173 * of combining a get/unset.
1175 * @param string $path path to property in meta data
1176 * @param mixed $default default value
1180 public function moveMeta( $path, $value = NULL )
1182 if ( isset( $this->__info[$path] ) ) {
1183 $value = $this->__info[ $path ];
1184 unset( $this->__info[ $path ] );
1190 * Stores a value in the specified Meta information property.
1191 * The first argument should be the key to store the value under,
1192 * the second argument should be the value. It is common to use
1193 * a path-like notation for meta data in RedBeanPHP like:
1194 * 'my.meta.data', however the dots are purely for readability, the
1195 * meta data methods do not store nested structures or hierarchies.
1197 * @param string $path path / key to store value under
1198 * @param mixed $value value to store in bean (not in database) as meta data
1202 public function setMeta( $path, $value )
1204 $this->__info[$path] = $value;
1210 * Copies the meta information of the specified bean
1211 * This is a convenience method to enable you to
1212 * exchange meta information easily.
1214 * @param OODBBean $bean bean to copy meta data of
1218 public function copyMetaFrom( OODBBean $bean )
1220 $this->__info = $bean->__info;
1226 * Sends the call to the registered model.
1227 * This method can also be used to override bean behaviour.
1228 * In that case you don't want an error or exception to be triggered
1229 * if the method does not exist in the model (because it's optional).
1230 * Unfortunately we cannot add an extra argument to __call() for this
1231 * because the signature is fixed. Another option would be to set
1232 * a special flag ( i.e. $this->isOptionalCall ) but that would
1233 * cause additional complexity because we have to deal with extra temporary state.
1234 * So, instead I allowed the method name to be prefixed with '@', in practice
1235 * nobody creates methods like that - however the '@' symbol in PHP is widely known
1236 * to suppress error handling, so we can reuse the semantics of this symbol.
1237 * If a method name gets passed starting with '@' the overrideDontFail variable
1238 * will be set to TRUE and the '@' will be stripped from the function name before
1239 * attempting to invoke the method on the model. This way, we have all the
1240 * logic in one place.
1242 * @param string $method name of the method
1243 * @param array $args argument list
1247 public function __call( $method, $args )
1249 $overrideDontFail = FALSE;
1250 if ( strpos( $method, '@' ) === 0 ) {
1251 $method = substr( $method, 1 );
1252 $overrideDontFail = TRUE;
1255 if ( !isset( $this->__info['model'] ) ) {
1256 $model = $this->beanHelper->getModelForBean( $this );
1262 $this->__info['model'] = $model;
1264 if ( !method_exists( $this->__info['model'], $method ) ) {
1266 if ( self::$errorHandlingFUSE === FALSE || $overrideDontFail ) {
1270 if ( in_array( $method, array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ), TRUE ) ) {
1274 $message = "FUSE: method does not exist in model: $method";
1275 if ( self::$errorHandlingFUSE === self::C_ERR_LOG ) {
1276 error_log( $message );
1278 } elseif ( self::$errorHandlingFUSE === self::C_ERR_NOTICE ) {
1279 trigger_error( $message, E_USER_NOTICE );
1281 } elseif ( self::$errorHandlingFUSE === self::C_ERR_WARN ) {
1282 trigger_error( $message, E_USER_WARNING );
1284 } elseif ( self::$errorHandlingFUSE === self::C_ERR_EXCEPTION ) {
1285 throw new \Exception( $message );
1286 } elseif ( self::$errorHandlingFUSE === self::C_ERR_FUNC ) {
1287 $func = self::$errorHandler;
1289 'message' => $message,
1290 'method' => $method,
1295 trigger_error( $message, E_USER_ERROR );
1299 return call_user_func_array( array( $this->__info['model'], $method ), $args );
1303 * Implementation of __toString Method
1304 * Routes call to Model. If the model implements a __toString() method this
1305 * method will be called and the result will be returned. In case of an
1306 * echo-statement this result will be printed. If the model does not
1307 * implement a __toString method, this method will return a JSON
1308 * representation of the current bean.
1312 public function __toString()
1314 $string = $this->__call( '@__toString', array() );
1316 if ( $string === NULL ) {
1318 foreach($this->properties as $property => $value) {
1319 if (is_scalar($value)) {
1320 $list[$property] = $value;
1323 return json_encode( $list );
1330 * Implementation of Array Access Interface, you can access bean objects
1332 * Call gets routed to __set.
1334 * @param mixed $offset offset string
1335 * @param mixed $value value
1339 public function offsetSet( $offset, $value )
1341 $this->__set( $offset, $value );
1345 * Implementation of Array Access Interface, you can access bean objects
1348 * Array functions do not reveal x-own-lists and list-alias because
1349 * you dont want duplicate entries in foreach-loops.
1350 * Also offers a slight performance improvement for array access.
1352 * @param mixed $offset property
1356 public function offsetExists( $offset )
1358 return $this->__isset( $offset );
1362 * Implementation of Array Access Interface, you can access bean objects
1364 * Unsets a value from the array/bean.
1366 * Array functions do not reveal x-own-lists and list-alias because
1367 * you dont want duplicate entries in foreach-loops.
1368 * Also offers a slight performance improvement for array access.
1370 * @param mixed $offset property
1374 public function offsetUnset( $offset )
1376 $this->__unset( $offset );
1380 * Implementation of Array Access Interface, you can access bean objects
1382 * Returns value of a property.
1384 * Array functions do not reveal x-own-lists and list-alias because
1385 * you dont want duplicate entries in foreach-loops.
1386 * Also offers a slight performance improvement for array access.
1388 * @param mixed $offset property
1392 public function &offsetGet( $offset )
1394 return $this->__get( $offset );
1398 * Chainable method to cast a certain ID to a bean; for instance:
1399 * $person = $club->fetchAs('person')->member;
1400 * This will load a bean of type person using member_id as ID.
1402 * @param string $type preferred fetch type
1406 public function fetchAs( $type )
1408 $this->fetchType = $type;
1414 * For polymorphic bean relations.
1415 * Same as fetchAs but uses a column instead of a direct value.
1417 * @param string $field field name to use for mapping
1421 public function poly( $field )
1423 return $this->fetchAs( $this->$field );
1427 * Traverses a bean property with the specified function.
1428 * Recursively iterates through the property invoking the
1429 * function for each bean along the way passing the bean to it.
1431 * Can be used together with with, withCondition, alias and fetchAs.
1433 * @param string $property property
1434 * @param callable $function function
1435 * @param integer $maxDepth maximum depth for traversal
1438 * @throws RedException
1440 public function traverse( $property, $function, $maxDepth = NULL )
1443 if ( strpos( $property, 'shared' ) !== FALSE ) {
1444 throw new RedException( 'Traverse only works with (x)own-lists.' );
1447 if ( !is_null( $maxDepth ) ) {
1448 if ( !$maxDepth-- ) return $this;
1451 $oldFetchType = $this->fetchType;
1452 $oldAliasName = $this->aliasName;
1453 $oldWith = $this->withSql;
1454 $oldBindings = $this->withParams;
1456 $beans = $this->$property;
1458 if ( $beans === NULL ) return $this;
1460 if ( !is_array( $beans ) ) $beans = array( $beans );
1462 foreach( $beans as $bean ) {
1463 /** @var OODBBean $bean */
1466 $bean->fetchType = $oldFetchType;
1467 $bean->aliasName = $oldAliasName;
1468 $bean->withSql = $oldWith;
1469 $bean->withParams = $oldBindings;
1471 $bean->traverse( $property, $function, $maxDepth );
1478 * Implementation of Countable interface. Makes it possible to use
1479 * count() function on a bean.
1483 public function count()
1485 return count( $this->properties );
1489 * Checks whether a bean is empty or not.
1490 * A bean is empty if it has no other properties than the id field OR
1491 * if all the other property are empty().
1495 public function isEmpty()
1498 foreach ( $this->properties as $key => $value ) {
1499 if ( $key == 'id' ) {
1502 if ( !empty( $value ) ) {
1513 * @param string $property the property of the bean
1514 * @param mixed $value the value you want to set
1518 public function setAttr( $property, $value )
1520 $this->$property = $value;
1527 * Unsets all properties in array.
1529 * @param array $properties properties you want to unset.
1533 public function unsetAll( $properties )
1535 foreach ( $properties as $prop ) {
1536 if ( isset( $this->properties[$prop] ) ) {
1537 unset( $this->properties[$prop] );
1545 * Returns original (old) value of a property.
1546 * You can use this method to see what has changed in a
1549 * @param string $property name of the property you want the old value of
1553 public function old( $property )
1555 $old = $this->getMeta( 'sys.orig', array() );
1557 if ( array_key_exists( $property, $old ) ) {
1558 return $old[$property];
1565 * Convenience method.
1566 * Returns TRUE if the bean has been changed, or FALSE otherwise.
1567 * Same as $bean->getMeta('tainted');
1568 * Note that a bean becomes tainted as soon as you retrieve a list from
1569 * the bean. This is because the bean lists are arrays and the bean cannot
1570 * determine whether you have made modifications to a list so RedBeanPHP
1571 * will mark the whole bean as tainted.
1575 public function isTainted()
1577 return $this->getMeta( 'tainted' );
1581 * Returns TRUE if the value of a certain property of the bean has been changed and
1584 * Note that this method will return TRUE if applied to a loaded list.
1585 * Also note that this method keeps track of the bean's history regardless whether
1586 * it has been stored or not. Storing a bean does not undo it's history,
1587 * to clean the history of a bean use: clearHistory().
1589 * @param string $property name of the property you want the change-status of
1593 public function hasChanged( $property )
1595 return ( array_key_exists( $property, $this->properties ) ) ?
1596 $this->old( $property ) != $this->properties[$property] : FALSE;
1600 * Returns TRUE if the specified list exists, has been loaded and has been changed:
1601 * beans have been added or deleted. This method will not tell you anything about
1602 * the state of the beans in the list.
1604 * @param string $property name of the list to check
1608 public function hasListChanged( $property )
1610 if ( !array_key_exists( $property, $this->properties ) ) return FALSE;
1611 $diffAdded = array_diff_assoc( $this->properties[$property], $this->__info['sys.shadow.'.$property] );
1612 if ( count( $diffAdded ) ) return TRUE;
1613 $diffMissing = array_diff_assoc( $this->__info['sys.shadow.'.$property], $this->properties[$property] );
1614 if ( count( $diffMissing ) ) return TRUE;
1619 * Clears (syncs) the history of the bean.
1620 * Resets all shadow values of the bean to their current value.
1624 public function clearHistory()
1626 $this->__info['sys.orig'] = array();
1627 foreach( $this->properties as $key => $value ) {
1628 if ( is_scalar($value) ) {
1629 $this->__info['sys.orig'][$key] = $value;
1631 $this->__info['sys.shadow.'.$key] = $value;
1638 * Creates a N-M relation by linking an intermediate bean.
1639 * This method can be used to quickly connect beans using indirect
1640 * relations. For instance, given an album and a song you can connect the two
1641 * using a track with a number like this:
1646 * $album->link('track', array('number'=>1))->song = $song;
1652 * $album->link($trackBean)->song = $song;
1655 * What this method does is adding the link bean to the own-list, in this case
1656 * ownTrack. If the first argument is a string and the second is an array or
1657 * a JSON string then the linking bean gets dispensed on-the-fly as seen in
1658 * example #1. After preparing the linking bean, the bean is returned thus
1659 * allowing the chained setter: ->song = $song.
1661 * @param string|OODBBean $type type of bean to dispense or the full bean
1662 * @param string|array $qualification JSON string or array (optional)
1666 public function link( $typeOrBean, $qualification = array() )
1668 if ( is_string( $typeOrBean ) ) {
1670 $typeOrBean = AQueryWriter::camelsSnake( $typeOrBean );
1672 $bean = $this->beanHelper->getToolBox()->getRedBean()->dispense( $typeOrBean );
1674 if ( is_string( $qualification ) ) {
1675 $data = json_decode( $qualification, TRUE );
1677 $data = $qualification;
1680 foreach ( $data as $key => $value ) {
1681 $bean->$key = $value;
1684 $bean = $typeOrBean;
1687 $list = 'own' . ucfirst( $bean->getMeta( 'type' ) );
1689 array_push( $this->$list, $bean );
1695 * Returns a bean of the given type with the same ID of as
1696 * the current one. This only happens in a one-to-one relation.
1697 * This is as far as support for 1-1 goes in RedBeanPHP. This
1698 * method will only return a reference to the bean, changing it
1699 * and storing the bean will not update the related one-bean.
1703 public function one( $type ) {
1704 return $this->beanHelper->getToolBox()->getRedBean()->load( $type, $this->id );
1708 * Returns the same bean freshly loaded from the database.
1712 public function fresh()
1714 return $this->beanHelper->getToolbox()->getRedBean()->load( $this->getMeta( 'type' ), $this->properties['id'] );
1718 * Registers a association renaming globally.
1720 * @param string $via type you wish to use for shared lists
1724 public function via( $via )
1726 $this->via = AQueryWriter::camelsSnake( $via );
1732 * Counts all own beans of type $type.
1733 * Also works with alias(), with() and withCondition().
1735 * @param string $type the type of bean you want to count
1739 public function countOwn( $type )
1741 $type = $this->beau( $type );
1743 if ( $this->aliasName ) {
1744 $myFieldLink = $this->aliasName . '_id';
1746 $this->aliasName = NULL;
1748 $myFieldLink = $this->__info['type'] . '_id';
1753 if ( $this->getID() ) {
1756 if ( count( $this->withParams ) > 0 ) {
1757 reset( $this->withParams );
1758 $firstKey = key( $this->withParams );
1761 $joinSql = $this->parseJoin( $type );
1763 if ( !is_numeric( $firstKey ) || $firstKey === NULL ) {
1764 $bindings = $this->withParams;
1765 $bindings[':slot0'] = $this->getID();
1766 $count = $this->beanHelper->getToolbox()->getWriter()->queryRecordCount( $type, array(), " {$joinSql} $myFieldLink = :slot0 " . $this->withSql, $bindings );
1768 $bindings = array_merge( array( $this->getID() ), $this->withParams );
1769 $count = $this->beanHelper->getToolbox()->getWriter()->queryRecordCount( $type, array(), " {$joinSql} $myFieldLink = ? " . $this->withSql, $bindings );
1774 $this->clearModifiers();
1775 return (int) $count;
1779 * Counts all shared beans of type $type.
1780 * Also works with via(), with() and withCondition().
1782 * @param string $type type of bean you wish to count
1786 public function countShared( $type )
1788 $toolbox = $this->beanHelper->getToolbox();
1789 $redbean = $toolbox->getRedBean();
1790 $writer = $toolbox->getWriter();
1793 $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) );
1795 if ( $oldName !== $this->via ) {
1796 //set the new renaming rule
1797 $writer->renameAssocTable( $oldName, $this->via );
1802 $type = $this->beau( $type );
1805 if ( $this->getID() ) {
1806 $count = $redbean->getAssociationManager()->relatedCount( $this, $type, $this->withSql, $this->withParams );
1809 $this->clearModifiers();
1810 return (integer) $count;
1814 * Iterates through the specified own-list and
1815 * fetches all properties (with their type) and
1816 * returns the references.
1817 * Use this method to quickly load indirectly related
1818 * beans in an own-list. Whenever you cannot use a
1819 * shared-list this method offers the same convenience
1820 * by aggregating the parent beans of all children in
1821 * the specified own-list.
1826 * $quest->aggr( 'xownQuestTarget', 'target', 'quest' );
1829 * Loads (in batch) and returns references to all
1830 * quest beans residing in the $questTarget->target properties
1831 * of each element in the xownQuestTargetList.
1833 * @param string $list the list you wish to process
1834 * @param string $property the property to load
1835 * @param string $type the type of bean residing in this property (optional)
1839 public function &aggr( $list, $property, $type = NULL )
1842 $ids = $beanIndex = $references = array();
1844 if ( strlen( $list ) < 4 ) throw new RedException('Invalid own-list.');
1845 if ( strpos( $list, 'own') !== 0 ) throw new RedException('Only own-lists can be aggregated.');
1846 if ( !ctype_upper( substr( $list, 3, 1 ) ) ) throw new RedException('Invalid own-list.');
1848 if ( is_null( $type ) ) $type = $property;
1850 foreach( $this->$list as $bean ) {
1851 $field = $property . '_id';
1852 if ( isset( $bean->$field) ) {
1853 $ids[] = $bean->$field;
1854 $beanIndex[$bean->$field] = $bean;
1858 $beans = $this->beanHelper->getToolBox()->getRedBean()->batch( $type, $ids );
1860 //now preload the beans as well
1861 foreach( $beans as $bean ) {
1862 $beanIndex[$bean->id]->setProperty( $property, $bean );
1865 foreach( $beanIndex as $indexedBean ) {
1866 $references[] = $indexedBean->$property;
1873 * Tests whether the database identities of two beans are equal.
1875 * @param OODBBean $bean other bean
1879 public function equals(OODBBean $bean)
1882 ( (string) $this->properties['id'] === (string) $bean->properties['id'] )
1883 && ( (string) $this->__info['type'] === (string) $bean->__info['type'] )
1888 * Magic method jsonSerialize, implementation for the \JsonSerializable interface,
1889 * this method gets called by json_encode and facilitates a better JSON representation
1890 * of the bean. Exports the bean on JSON serialization, for the JSON fans.
1892 * @see http://php.net/manual/en/class.jsonserializable.php
1896 public function jsonSerialize()
1898 return $this->export();