<?php
namespace RedBeanPHP;
use RedBeanPHP\Adapter\DBAdapter as DBAdapter;
use RedBeanPHP\QueryWriter as QueryWriter;
use RedBeanPHP\BeanHelper as BeanHelper;
use RedBeanPHP\RedException\SQL as SQLException;
use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter;
use RedBeanPHP\Cursor as Cursor;
use RedBeanPHP\Cursor\NullCursor as NullCursor;
/**
* Abstract Repository.
*
* OODB manages two repositories, a fluid one that
* adjust the database schema on-the-fly to accommodate for
* new bean types (tables) and new properties (columns) and
* a frozen one for use in a production environment. OODB
* allows you to swap the repository instances using the freeze()
* method.
*
* @file RedBeanPHP/Repository.php
* @author Gabor de Mooij and the RedBeanPHP community
* @license BSD/GPLv2
*
* @copyright
* copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community
* This source file is subject to the BSD/GPLv2 License that is bundled
* with this source code in the file license.txt.
*/
abstract class Repository
{
/**
* @var array
*/
protected $stash = NULL;
/**
* @var integer
*/
protected $nesting = 0;
/**
* @var QueryWriter
*/
protected $writer;
/**
* @var boolean|array
*/
protected $partialBeans = FALSE;
/**
* @var OODB
*/
public $oodb = NULL;
/**
* Toggles 'partial bean mode'. If this mode has been
* selected the repository will only update the fields of a bean that
* have been changed rather than the entire bean.
* Pass the value TRUE to select 'partial mode' for all beans.
* Pass the value FALSE to disable 'partial mode'.
* Pass an array of bean types if you wish to use partial mode only
* for some types.
* This method will return the previous value.
*
* @param boolean|array $yesNoBeans List of type names or 'all'
*
* @return boolean|array
*/
public function usePartialBeans( $yesNoBeans )
{
$oldValue = $this->partialBeans;
$this->partialBeans = $yesNoBeans;
return $oldValue;
}
/**
* Fully processes a bean and updates the associated records in the database.
* First the bean properties will be grouped as 'embedded' bean,
* addition, deleted 'trash can' or residue. Next, the different groups
* of beans will be processed accordingly and the reference bean (i.e.
* the one that was passed to the method as an argument) will be stored.
* Each type of list (own/shared) has 3 bean processors:
*
* - trashCanProcessor : removes the bean or breaks its association with the current bean
* - additionProcessor : associates the bean with the current one
* - residueProcessor : manages beans in lists that 'remain' but may need to be updated
*
* This method first groups the beans and then calls the
* internal processing methods.
*
* @param OODBBean $bean bean to process
*
* @return void
*/
protected function storeBeanWithLists( OODBBean $bean )
{
$sharedAdditions = $sharedTrashcan = $sharedresidue = $sharedItems = $ownAdditions = $ownTrashcan = $ownresidue = $embeddedBeans = array(); //Define groups
foreach ( $bean as $property => $value ) {
$value = ( $value instanceof SimpleModelInterface ) ? $value->unbox() : $value;
if ( $value instanceof OODBBean ) {
$this->processEmbeddedBean( $embeddedBeans, $bean, $property, $value );
$bean->setMeta("sys.typeof.{$property}", $value->getMeta('type'));
} elseif ( is_array( $value ) ) {
foreach($value as &$item) {
$item = ( $item instanceof SimpleModelInterface ) ? $item->unbox() : $item;
}
$originals = $bean->moveMeta( 'sys.shadow.' . $property, array() );
if ( strpos( $property, 'own' ) === 0 ) {
list( $ownAdditions, $ownTrashcan, $ownresidue ) = $this->processGroups( $originals, $value, $ownAdditions, $ownTrashcan, $ownresidue );
$listName = lcfirst( substr( $property, 3 ) );
if ($bean->moveMeta( 'sys.exclusive-'. $listName ) ) {
OODBBean::setMetaAll( $ownTrashcan, 'sys.garbage', TRUE );
OODBBean::setMetaAll( $ownAdditions, 'sys.buildcommand.fkdependson', $bean->getMeta( 'type' ) );
}
unset( $bean->$property );
} elseif ( strpos( $property, 'shared' ) === 0 ) {
list( $sharedAdditions, $sharedTrashcan, $sharedresidue ) = $this->processGroups( $originals, $value, $sharedAdditions, $sharedTrashcan, $sharedresidue );
unset( $bean->$property );
}
}
}
$this->storeBean( $bean );
$this->processTrashcan( $bean, $ownTrashcan );
$this->processAdditions( $bean, $ownAdditions );
$this->processResidue( $ownresidue );
$this->processSharedTrashcan( $bean, $sharedTrashcan );
$this->processSharedAdditions( $bean, $sharedAdditions );
$this->processSharedResidue( $bean, $sharedresidue );
}
/**
* Process groups. Internal function. Processes different kind of groups for
* storage function. Given a list of original beans and a list of current beans,
* this function calculates which beans remain in the list (residue), which
* have been deleted (are in the trashcan) and which beans have been added
* (additions).
*
* @param array $originals originals
* @param array $current the current beans
* @param array $additions beans that have been added
* @param array $trashcan beans that have been deleted
* @param array $residue beans that have been left untouched
*
* @return array
*/
protected function processGroups( $originals, $current, $additions, $trashcan, $residue )
{
return array(
array_merge( $additions, array_diff( $current, $originals ) ),
array_merge( $trashcan, array_diff( $originals, $current ) ),
array_merge( $residue, array_intersect( $current, $originals ) )
);
}
/**
* Processes a list of beans from a bean.
* A bean may contain lists. This
* method handles shared addition lists; i.e.
* the $bean->sharedObject properties.
* Shared beans will be associated with each other using the
* Association Manager.
*
* @param OODBBean $bean the bean
* @param array $sharedAdditions list with shared additions
*
* @return void
*/
protected function processSharedAdditions( $bean, $sharedAdditions )
{
foreach ( $sharedAdditions as $addition ) {
if ( $addition instanceof OODBBean ) {
$this->oodb->getAssociationManager()->associate( $addition, $bean );
} else {
throw new RedException( 'Array may only contain OODBBeans' );
}
}
}
/**
* Processes a list of beans from a bean.
* A bean may contain lists. This
* method handles own lists; i.e.
* the $bean->ownObject properties.
* A residue is a bean in an own-list that stays
* where it is. This method checks if there have been any
* modification to this bean, in that case
* the bean is stored once again, otherwise the bean will be left untouched.
*
* @param array $ownresidue list to process
*
* @return void
*/
protected function processResidue( $ownresidue )
{
foreach ( $ownresidue as $residue ) {
if ( $residue->getMeta( 'tainted' ) ) {
$this->store( $residue );
}
}
}
/**
* Processes a list of beans from a bean. A bean may contain lists. This
* method handles own lists; i.e. the $bean->ownObject properties.
* A trash can bean is a bean in an own-list that has been removed
* (when checked with the shadow). This method
* checks if the bean is also in the dependency list. If it is the bean will be removed.
* If not, the connection between the bean and the owner bean will be broken by
* setting the ID to NULL.
*
* @param OODBBean $bean bean to process
* @param array $ownTrashcan list to process
*
* @return void
*/
protected function processTrashcan( $bean, $ownTrashcan )
{
foreach ( $ownTrashcan as $trash ) {
$myFieldLink = $bean->getMeta( 'type' ) . '_id';
$alias = $bean->getMeta( 'sys.alias.' . $trash->getMeta( 'type' ) );
if ( $alias ) $myFieldLink = $alias . '_id';
if ( $trash->getMeta( 'sys.garbage' ) === TRUE ) {
$this->trash( $trash );
} else {
$trash->$myFieldLink = NULL;
$this->store( $trash );
}
}
}
/**
* Unassociates the list items in the trashcan.
* This bean processor processes the beans in the shared trash can.
* This group of beans has been deleted from a shared list.
* The affected beans will no longer be associated with the bean
* that contains the shared list.
*
* @param OODBBean $bean bean to process
* @param array $sharedTrashcan list to process
*
* @return void
*/
protected function processSharedTrashcan( $bean, $sharedTrashcan )
{
foreach ( $sharedTrashcan as $trash ) {
$this->oodb->getAssociationManager()->unassociate( $trash, $bean );
}
}
/**
* Stores all the beans in the residue group.
* This bean processor processes the beans in the shared residue
* group. This group of beans 'remains' in the list but might need
* to be updated or synced. The affected beans will be stored
* to perform the required database queries.
*
* @param OODBBean $bean bean to process
* @param array $sharedresidue list to process
*
* @return void
*/
protected function processSharedResidue( $bean, $sharedresidue )
{
foreach ( $sharedresidue as $residue ) {
$this->store( $residue );
}
}
/**
* Determines whether the bean has 'loaded lists' or
* 'loaded embedded beans' that need to be processed
* by the store() method.
*
* @param OODBBean $bean bean to be examined
*
* @return boolean
*/
protected function hasListsOrObjects( OODBBean $bean )
{
$processLists = FALSE;
foreach ( $bean as $value ) {
if ( is_array( $value ) || is_object( $value ) ) {
$processLists = TRUE;
break;
}
}
return $processLists;
}
/**
* Converts an embedded bean to an ID, removes the bean property and
* stores the bean in the embedded beans array. The id will be
* assigned to the link field property, i.e. 'bean_id'.
*
* @param array $embeddedBeans destination array for embedded bean
* @param OODBBean $bean target bean to process
* @param string $property property that contains the embedded bean
* @param OODBBean $value embedded bean itself
*
* @return void
*/
protected function processEmbeddedBean( &$embeddedBeans, $bean, $property, OODBBean $value )
{
$linkField = $property . '_id';
if ( !$value->id || $value->getMeta( 'tainted' ) ) {
$this->store( $value );
}
$id = $value->id;
if ($bean->$linkField != $id) $bean->$linkField = $id;
$bean->setMeta( 'cast.' . $linkField, 'id' );
$embeddedBeans[$linkField] = $value;
unset( $bean->$property );
}
/**
* Constructor, requires a query writer and OODB.
* Creates a new instance of the bean respository class.
*
* @param OODB $oodb instance of object database
* @param QueryWriter $writer the Query Writer to use for this repository
*
* @return void
*/
public function __construct( OODB $oodb, QueryWriter $writer )
{
$this->writer = $writer;
$this->oodb = $oodb;
}
/**
* Checks whether a OODBBean bean is valid.
* If the type is not valid or the ID is not valid it will
* throw an exception: Security. To be valid a bean
* must abide to the following rules:
*
* - It must have an primary key id property named: id
* - It must have a type
* - The type must conform to the RedBeanPHP naming policy
* - All properties must be valid
* - All values must be valid
*
* @param OODBBean $bean the bean that needs to be checked
*
* @return void
*/
public function check( OODBBean $bean )
{
//Is all meta information present?
if ( !isset( $bean->id ) ) {
throw new RedException( 'Bean has incomplete Meta Information id ' );
}
if ( !( $bean->getMeta( 'type' ) ) ) {
throw new RedException( 'Bean has incomplete Meta Information II' );
}
//Pattern of allowed characters
$pattern = '/[^a-z0-9_]/i';
//Does the type contain invalid characters?
if ( preg_match( $pattern, $bean->getMeta( 'type' ) ) ) {
throw new RedException( 'Bean Type is invalid' );
}
//Are the properties and values valid?
foreach ( $bean as $prop => $value ) {
if (
is_array( $value )
|| ( is_object( $value ) )
) {
throw new RedException( "Invalid Bean value: property $prop" );
} else if (
strlen( $prop ) < 1
|| preg_match( $pattern, $prop )
) {
throw new RedException( "Invalid Bean property: property $prop" );
}
}
}
/**
* Dispenses a new bean (a OODBBean Bean Object)
* of the specified type. Always
* use this function to get an empty bean object. Never
* instantiate a OODBBean yourself because it needs
* to be configured before you can use it with RedBean. This
* function applies the appropriate initialization /
* configuration for you.
*
* To use a different class for beans (instead of OODBBean) set:
* REDBEAN_OODBBEAN_CLASS to the name of the class to be used.
*
* @param string $type type of bean you want to dispense
* @param int $number number of beans you would like to get
* @param boolean $alwaysReturnArray if TRUE always returns the result as an array
*
* @return OODBBean|OODBBean[]
*/
public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE )
{
$OODBBEAN = defined( 'REDBEAN_OODBBEAN_CLASS' ) ? REDBEAN_OODBBEAN_CLASS : '\RedBeanPHP\OODBBean';
$beans = array();
for ( $i = 0; $i < $number; $i++ ) {
$bean = new $OODBBEAN;
$bean->initializeForDispense( $type, $this->oodb->getBeanHelper() );
$this->check( $bean );
$this->oodb->signal( 'dispense', $bean );
$beans[] = $bean;
}
return ( count( $beans ) === 1 && !$alwaysReturnArray ) ? array_pop( $beans ) : $beans;
}
/**
* Searches the database for a bean that matches conditions $conditions and sql $addSQL
* and returns an array containing all the beans that have been found.
*
* Conditions need to take form:
*
* <code>
* array(
* 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' )
* 'PROPERTY' => array( POSSIBLE VALUES... )
* );
* </code>
*
* All conditions are glued together using the AND-operator, while all value lists
* are glued using IN-operators thus acting as OR-conditions.
*
* Note that you can use property names; the columns will be extracted using the
* appropriate bean formatter.
*
* @param string $type type of beans you are looking for
* @param array $conditions list of conditions
* @param string $sql SQL to be used in query
* @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not)
*
* @return array
*/
public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() )
{
//for backward compatibility, allow mismatch arguments:
if ( is_array( $sql ) ) {
if ( isset( $sql[1] ) ) {
$bindings = $sql[1];
}
$sql = $sql[0];
}
try {
$beans = $this->convertToBeans( $type, $this->writer->queryRecord( $type, $conditions, $sql, $bindings ) );
return $beans;
} catch ( SQLException $exception ) {
$this->handleException( $exception );
}
return array();
}
/**
* Finds a BeanCollection.
* Given a type, an SQL snippet and optionally some parameter bindings
* this methods returns a BeanCollection for your query.
*
* The BeanCollection represents a collection of beans and
* makes it possible to use database cursors. The BeanCollection
* has a method next() to obtain the first, next and last bean
* in the collection. The BeanCollection does not implement the array
* interface nor does it try to act like an array because it cannot go
* backward or rewind itself.
*
* @param string $type type of beans you are looking for
* @param string $sql SQL to be used in query
* @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not)
*
* @return BeanCollection
*/
public function findCollection( $type, $sql, $bindings = array() )
{
try {
$cursor = $this->writer->queryRecordWithCursor( $type, $sql, $bindings );
return new BeanCollection( $type, $this, $cursor );
} catch ( SQLException $exception ) {
$this->handleException( $exception );
}
return new BeanCollection( $type, $this, new NullCursor );
}
/**
* Stores a bean in the database. This method takes a
* OODBBean Bean Object $bean and stores it
* in the database. If the database schema is not compatible
* with this bean and RedBean runs in fluid mode the schema
* will be altered to store the bean correctly.
* If the database schema is not compatible with this bean and
* RedBean runs in frozen mode it will throw an exception.
* This function returns the primary key ID of the inserted
* bean.
*
* The return value is an integer if possible. If it is not possible to
* represent the value as an integer a string will be returned. We use
* explicit casts instead of functions to preserve performance
* (0.13 vs 0.28 for 10000 iterations on Core i3).
*
* @param OODBBean|SimpleModel|SimpleModelInterface $bean bean to store
*
* @return integer|string
*/
public function store( $bean )
{
$processLists = $this->hasListsOrObjects( $bean );
if ( !$processLists && !$bean->getMeta( 'tainted' ) ) {
return $bean->getID(); //bail out!
}
$this->oodb->signal( 'update', $bean );
$processLists = $this->hasListsOrObjects( $bean ); //check again, might have changed by model!
if ( $processLists ) {
$this->storeBeanWithLists( $bean );
} else {
$this->storeBean( $bean );
}
$this->oodb->signal( 'after_update', $bean );
return ( (string) $bean->id === (string) (int) $bean->id ) ? (int) $bean->id : (string) $bean->id;
}
/**
* Returns an array of beans. Pass a type and a series of ids and
* this method will bring you the corresponding beans.
*
* important note: Because this method loads beans using the load()
* function (but faster) it will return empty beans with ID 0 for
* every bean that could not be located. The resulting beans will have the
* passed IDs as their keys.
*
* @param string $type type of beans
* @param array $ids ids to load
*
* @return array
*/
public function batch( $type, $ids )
{
if ( !$ids ) {
return array();
}
$collection = array();
try {
$rows = $this->writer->queryRecord( $type, array( 'id' => $ids ) );
} catch ( SQLException $e ) {
$this->handleException( $e );
$rows = FALSE;
}
$this->stash[$this->nesting] = array();
if ( !$rows ) {
return array();
}
foreach ( $rows as $row ) {
$this->stash[$this->nesting][$row['id']] = $row;
}
foreach ( $ids as $id ) {
$collection[$id] = $this->load( $type, $id );
}
$this->stash[$this->nesting] = NULL;
return $collection;
}
/**
* This is a convenience method; it converts database rows
* (arrays) into beans. Given a type and a set of rows this method
* will return an array of beans of the specified type loaded with
* the data fields provided by the result set from the database.
*
* New in 4.3.2: meta mask. The meta mask is a special mask to send
* data from raw result rows to the meta store of the bean. This is
* useful for bundling additional information with custom queries.
* Values of every column who's name starts with $mask will be
* transferred to the meta section of the bean under key 'data.bundle'.
*
* @param string $type type of beans you would like to have
* @param array $rows rows from the database result
* @param string $mask meta mask to apply (optional)
*
* @return array
*/
public function convertToBeans( $type, $rows, $mask = '__meta' )
{
$masktype = gettype( $mask );
switch ( $masktype ) {
case 'string':
break;
case 'array':
$maskflip = array();
foreach ( $mask as $m ) {
if ( !is_string( $m ) ) {
$mask = NULL;
$masktype = 'NULL';
break 2;
}
$maskflip[$m] = TRUE;
}
$mask = $maskflip;
break;
default:
$mask = NULL;
$masktype = 'NULL';
}
$collection = array();
$this->stash[$this->nesting] = array();
foreach ( $rows as $row ) {
if ( $mask !== NULL ) {
$meta = array();
foreach( $row as $key => $value ) {
if ( $masktype === 'string' ) {
if ( strpos( $key, $mask ) === 0 ) {
unset( $row[$key] );
$meta[$key] = $value;
}
} elseif ( $masktype === 'array' ) {
if ( isset( $mask[$key] ) ) {
unset( $row[$key] );
$meta[$key] = $value;
}
}
}
}
$id = $row['id'];
$this->stash[$this->nesting][$id] = $row;
$collection[$id] = $this->load( $type, $id );
if ( $mask !== NULL ) {
$collection[$id]->setMeta( 'data.bundle', $meta );
}
}
$this->stash[$this->nesting] = NULL;
return $collection;
}
/**
* Counts the number of beans of type $type.
* This method accepts a second argument to modify the count-query.
* A third argument can be used to provide bindings for the SQL snippet.
*
* @param string $type type of bean we are looking for
* @param string $addSQL additional SQL snippet
* @param array $bindings parameters to bind to SQL
*
* @return integer
*/
public function count( $type, $addSQL = '', $bindings = array() )
{
$type = AQueryWriter::camelsSnake( $type );
try {
$count = (int) $this->writer->queryRecordCount( $type, array(), $addSQL, $bindings );
} catch ( SQLException $exception ) {
$this->handleException( $exception );
$count = 0;
}
return $count;
}
/**
* Removes a bean from the database.
* This function will remove the specified OODBBean
* Bean Object from the database.
*
* @param OODBBean|SimpleModel|SimpleModelInterface $bean bean you want to remove from database
*
* @return int
*/
public function trash( $bean )
{
$this->oodb->signal( 'delete', $bean );
foreach ( $bean as $property => $value ) {
if ( $value instanceof OODBBean ) {
unset( $bean->$property );
}
if ( is_array( $value ) ) {
if ( strpos( $property, 'own' ) === 0 ) {
unset( $bean->$property );
} elseif ( strpos( $property, 'shared' ) === 0 ) {
unset( $bean->$property );
}
}
}
try {
$deleted = $this->writer->deleteRecord( $bean->getMeta( 'type' ), array( 'id' => array( $bean->id ) ) );
} catch ( SQLException $exception ) {
$this->handleException( $exception );
}
$bean->id = 0;
$this->oodb->signal( 'after_delete', $bean );
return isset($deleted) ? $deleted : 0;
}
/**
* Checks whether the specified table already exists in the database.
* Not part of the Object Database interface!
*
* @param string $table table name
*
* @return boolean
*/
public function tableExists( $table )
{
return $this->writer->tableExists( $table );
}
/**
* Trash all beans of a given type.
* Wipes an entire type of bean. After this operation there
* will be no beans left of the specified type.
* This method will ignore exceptions caused by database
* tables that do not exist.
*
* @param string $type type of bean you wish to delete all instances of
*
* @return boolean
*/
public function wipe( $type )
{
try {
$this->writer->wipe( $type );
return TRUE;
} catch ( SQLException $exception ) {
if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ), $exception->getDriverDetails() ) ) {
throw $exception;
}
return FALSE;
}
}
}
|