mode === self::C_LOGGER_ECHO ) { echo $log; } else { $this->logs[] = $log; } } else { if ( $this->mode === self::C_LOGGER_ECHO ) { echo $argument; } else { $this->logs[] = $argument; } } if ( $this->mode === self::C_LOGGER_ECHO ) echo "
" . PHP_EOL; } } /** * Returns the internal log array. * The internal log array is where all log messages are stored. * * @return array */ public function getLogs() { return $this->logs; } /** * Clears the internal log array, removing all * previously stored entries. * * @return self */ public function clear() { $this->logs = array(); return $this; } /** * Selects a logging mode. * There are several options available. * * * C_LOGGER_ARRAY - log silently, stores entries in internal log array only * * C_LOGGER_ECHO - also forward log messages directly to STDOUT * * @param integer $mode mode of operation for logging object * * @return self */ public function setMode( $mode ) { if ($mode !== self::C_LOGGER_ARRAY && $mode !== self::C_LOGGER_ECHO ) { throw new RedException( 'Invalid mode selected for logger, use C_LOGGER_ARRAY or C_LOGGER_ECHO.' ); } $this->mode = $mode; return $this; } /** * Searches for all log entries in internal log array * for $needle and returns those entries. * This method will return an array containing all matches for your * search query. * * @param string $needle phrase to look for in internal log array * * @return array */ public function grep( $needle ) { $found = array(); foreach( $this->logs as $logEntry ) { if ( strpos( $logEntry, $needle ) !== FALSE ) $found[] = $logEntry; } return $found; } } } namespace RedBeanPHP\Logger\RDefault { use RedBeanPHP\Logger as Logger; use RedBeanPHP\Logger\RDefault as RDefault; use RedBeanPHP\RedException as RedException; /** * Debug logger. * A special logger for debugging purposes. * Provides debugging logging functions for RedBeanPHP. * * @file RedBeanPHP/Logger/RDefault/Debug.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @copyright * copyright (c) G.J.G.T. (Gabor) de Mooij * This source file is subject to the BSD/GPLv2 License that is bundled * with this source code in the file license.txt. */ class Debug extends RDefault implements Logger { /** * @var integer */ private $strLen = 40; /** * Writes a query for logging with all bindings / params filled * in. * * @param string $newSql the query * @param array $newBindings the bindings to process (key-value pairs) * * @return string */ private function writeQuery( $newSql, $newBindings ) { //avoid str_replace collisions: slot1 and slot10 (issue 407). uksort( $newBindings, function( $a, $b ) { return ( strlen( $b ) - strlen( $a ) ); } ); $newStr = $newSql; foreach( $newBindings as $slot => $value ) { if ( strpos( $slot, ':' ) === 0 ) { $newStr = str_replace( $slot, $this->fillInValue( $value ), $newStr ); } } return $newStr; } /** * Fills in a value of a binding and truncates the * resulting string if necessary. * * @param mixed $value bound value * * @return string */ protected function fillInValue( $value ) { if ( is_null( $value ) ) $value = 'NULL'; $value = strval( $value ); if ( strlen( $value ) > ( $this->strLen ) ) { $value = substr( $value, 0, ( $this->strLen ) ).'... '; } if ( !\RedBeanPHP\QueryWriter\AQueryWriter::canBeTreatedAsInt( $value ) && $value !== 'NULL') { $value = '\''.$value.'\''; } return $value; } /** * Dependending on the current mode of operation, * this method will either log and output to STDIN or * just log. * * Depending on the value of constant PHP_SAPI this function * will format output for console or HTML. * * @param string $str string to log or output and log * * @return void */ protected function output( $str ) { $this->logs[] = $str; if ( !$this->mode ) { $highlight = FALSE; /* just a quick heuritsic to highlight schema changes */ if ( strpos( $str, 'CREATE' ) === 0 || strpos( $str, 'ALTER' ) === 0 || strpos( $str, 'DROP' ) === 0) { $highlight = TRUE; } if (PHP_SAPI === 'cli') { if ($highlight) echo "\e[91m"; echo $str, PHP_EOL; echo "\e[39m"; } else { if ($highlight) { echo "{$str}"; } else { echo $str; } echo '
'; } } } /** * Normalizes the slots in an SQL string. * Replaces question mark slots with :slot1 :slot2 etc. * * @param string $sql sql to normalize * * @return string */ protected function normalizeSlots( $sql ) { $newSql = $sql; $i = 0; while(strpos($newSql, '?') !== FALSE ){ $pos = strpos( $newSql, '?' ); $slot = ':slot'.$i; $begin = substr( $newSql, 0, $pos ); $end = substr( $newSql, $pos+1 ); if (PHP_SAPI === 'cli') { $newSql = "{$begin}\e[32m{$slot}\e[39m{$end}"; } else { $newSql = "{$begin}$slot{$end}"; } $i ++; } return $newSql; } /** * Normalizes the bindings. * Replaces numeric binding keys with :slot1 :slot2 etc. * * @param array $bindings bindings to normalize * * @return array */ protected function normalizeBindings( $bindings ) { $i = 0; $newBindings = array(); foreach( $bindings as $key => $value ) { if ( is_numeric($key) ) { $newKey = ':slot'.$i; $newBindings[$newKey] = $value; $i++; } else { $newBindings[$key] = $value; } } return $newBindings; } /** * Logger method. * * Takes a number of arguments tries to create * a proper debug log based on the available data. * * @return void */ public function log() { if ( func_num_args() < 1 ) return; $sql = func_get_arg( 0 ); if ( func_num_args() < 2) { $bindings = array(); } else { $bindings = func_get_arg( 1 ); } if ( !is_array( $bindings ) ) { return $this->output( $sql ); } $newSql = $this->normalizeSlots( $sql ); $newBindings = $this->normalizeBindings( $bindings ); $newStr = $this->writeQuery( $newSql, $newBindings ); $this->output( $newStr ); } /** * Sets the max string length for the parameter output in * SQL queries. Set this value to a reasonable number to * keep you SQL queries readable. * * @param integer $len string length * * @return self */ public function setParamStringLength( $len = 20 ) { $this->strLen = max(0, $len); return $this; } } } namespace RedBeanPHP { /** * Interface for database drivers. * The Driver API conforms to the ADODB pseudo standard * for database drivers. * * @file RedBeanPHP/Driver.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. */ interface Driver { /** * Runs a query and fetches results as a multi dimensional array. * * @param string $sql SQL query to execute * @param array $bindings list of values to bind to SQL snippet * * @return array */ public function GetAll( $sql, $bindings = array() ); /** * Runs a query and fetches results as a column. * * @param string $sql SQL query to execute * @param array $bindings list of values to bind to SQL snippet * * @return array */ public function GetCol( $sql, $bindings = array() ); /** * Runs a query and returns results as a single cell. * * @param string $sql SQL query to execute * @param array $bindings list of values to bind to SQL snippet * * @return mixed */ public function GetOne( $sql, $bindings = array() ); /** * Runs a query and returns results as an associative array * indexed by the first column. * * @param string $sql SQL query to execute * @param array $bindings list of values to bind to SQL snippet * * @return mixed */ public function GetAssocRow( $sql, $bindings = array() ); /** * Runs a query and returns a flat array containing the values of * one row. * * @param string $sql SQL query to execute * @param array $bindings list of values to bind to SQL snippet * * @return array */ public function GetRow( $sql, $bindings = array() ); /** * Executes SQL code and allows key-value binding. * This function allows you to provide an array with values to bind * to query parameters. For instance you can bind values to question * marks in the query. Each value in the array corresponds to the * question mark in the query that matches the position of the value in the * array. You can also bind values using explicit keys, for instance * array(":key"=>123) will bind the integer 123 to the key :key in the * SQL. This method has no return value. * * @param string $sql SQL query to execute * @param array $bindings list of values to bind to SQL snippet * * @return array Affected Rows */ public function Execute( $sql, $bindings = array() ); /** * Returns the latest insert ID if driver does support this * feature. * * @return integer */ public function GetInsertID(); /** * Returns the number of rows affected by the most recent query * if the currently selected driver driver supports this feature. * * @return integer */ public function Affected_Rows(); /** * Returns a cursor-like object from the database. * * @param string $sql SQL query to execute * @param array $bindings list of values to bind to SQL snippet * * @return mixed */ public function GetCursor( $sql, $bindings = array() ); /** * Toggles debug mode. In debug mode the driver will print all * SQL to the screen together with some information about the * results. All SQL code that passes through the driver will be * passes on to the screen for inspection. * This method has no return value. * * @param boolean $tf TRUE = debug mode ON * @param Logger $customLogger * * @return void */ public function setDebugMode( $tf, $customLogger ); /** * Starts a transaction. * * @return void */ public function CommitTrans(); /** * Commits a transaction. * * @return void */ public function StartTrans(); /** * Rolls back a transaction. * * @return void */ public function FailTrans(); /** * Resets the internal Query Counter. * * @return self */ public function resetCounter(); /** * Returns the number of SQL queries processed. * * @return integer */ public function getQueryCount(); } } namespace RedBeanPHP\Driver { use RedBeanPHP\Driver as Driver; use RedBeanPHP\Logger as Logger; use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; use RedBeanPHP\RedException as RedException; use RedBeanPHP\RedException\SQL as SQL; use RedBeanPHP\Logger\RDefault as RDefault; use RedBeanPHP\PDOCompatible as PDOCompatible; use RedBeanPHP\Cursor\PDOCursor as PDOCursor; /** * PDO Driver * This Driver implements the RedBean Driver API. * for RedBeanPHP. This is the standard / default database driver * for RedBeanPHP. * * @file RedBeanPHP/PDO.php * @author Gabor de Mooij and the RedBeanPHP Community, Desfrenes * @license BSD/GPLv2 * * @copyright * copyright (c) Desfrenes & 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. */ class RPDO implements Driver { /** * @var integer */ protected $max; /** * @var string */ protected $dsn; /** * @var boolean */ protected $loggingEnabled = FALSE; /** * @var Logger */ protected $logger = NULL; /** * @var PDO */ protected $pdo; /** * @var integer */ protected $affectedRows; /** * @var integer */ protected $resultArray; /** * @var array */ protected $connectInfo = array(); /** * @var boolean */ protected $isConnected = FALSE; /** * @var bool */ protected $flagUseStringOnlyBinding = FALSE; /** * @var integer */ protected $queryCounter = 0; /** * @var string */ protected $mysqlCharset = ''; /** * @var string */ protected $mysqlCollate = ''; /** * @var boolean */ protected $stringifyFetches = TRUE; /** * @var string */ protected $initSQL = NULL; /** * Binds parameters. This method binds parameters to a PDOStatement for * Query Execution. This method binds parameters as NULL, INTEGER or STRING * and supports both named keys and question mark keys. * * @param PDOStatement $statement PDO Statement instance * @param array $bindings values that need to get bound to the statement * * @return void */ protected function bindParams( $statement, $bindings ) { foreach ( $bindings as $key => &$value ) { if ( is_integer( $key ) ) { if ( is_null( $value ) ) { $statement->bindValue( $key + 1, NULL, \PDO::PARAM_NULL ); } elseif ( !$this->flagUseStringOnlyBinding && AQueryWriter::canBeTreatedAsInt( $value ) && abs( $value ) <= $this->max ) { $statement->bindParam( $key + 1, $value, \PDO::PARAM_INT ); } else { $statement->bindParam( $key + 1, $value, \PDO::PARAM_STR ); } } else { if ( is_null( $value ) ) { $statement->bindValue( $key, NULL, \PDO::PARAM_NULL ); } elseif ( !$this->flagUseStringOnlyBinding && AQueryWriter::canBeTreatedAsInt( $value ) && abs( $value ) <= $this->max ) { $statement->bindParam( $key, $value, \PDO::PARAM_INT ); } else { $statement->bindParam( $key, $value, \PDO::PARAM_STR ); } } } } /** * This method runs the actual SQL query and binds a list of parameters to the query. * slots. The result of the query will be stored in the protected property * $rs (always array). The number of rows affected (result of rowcount, if supported by database) * is stored in protected property $affectedRows. If the debug flag is set * this function will send debugging output to screen buffer. * * @param string $sql the SQL string to be send to database server * @param array $bindings the values that need to get bound to the query slots * @param array $options * * @return mixed * @throws SQL */ protected function runQuery( $sql, $bindings, $options = array() ) { $this->connect(); if ( $this->loggingEnabled && $this->logger ) { $this->logger->log( $sql, $bindings ); } try { if ( strpos( 'pgsql', $this->dsn ) === 0 ) { if ( defined( '\PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT' ) ) { $statement = $this->pdo->prepare( $sql, array( \PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT => TRUE ) ); } else { $statement = $this->pdo->prepare( $sql ); } } else { $statement = $this->pdo->prepare( $sql ); } $this->bindParams( $statement, $bindings ); $statement->execute(); $this->queryCounter ++; $this->affectedRows = $statement->rowCount(); if ( $statement->columnCount() ) { $fetchStyle = ( isset( $options['fetchStyle'] ) ) ? $options['fetchStyle'] : NULL; if ( isset( $options['noFetch'] ) && $options['noFetch'] ) { $this->resultArray = array(); return $statement; } $this->resultArray = $statement->fetchAll( $fetchStyle ); if ( $this->loggingEnabled && $this->logger ) { $this->logger->log( 'resultset: ' . count( $this->resultArray ) . ' rows' ); } } else { $this->resultArray = array(); } } catch ( \PDOException $e ) { //Unfortunately the code field is supposed to be int by default (php) //So we need a property to convey the SQL State code. $err = $e->getMessage(); if ( $this->loggingEnabled && $this->logger ) $this->logger->log( 'An error occurred: ' . $err ); $exception = new SQL( $err, 0, $e ); $exception->setSQLState( $e->getCode() ); $exception->setDriverDetails( $e->errorInfo ); throw $exception; } } /** * Try to fix MySQL character encoding problems. * MySQL < 5.5.3 does not support proper 4 byte unicode but they * seem to have added it with version 5.5.3 under a different label: utf8mb4. * We try to select the best possible charset based on your version data. * * @return void */ protected function setEncoding() { $driver = $this->pdo->getAttribute( \PDO::ATTR_DRIVER_NAME ); if ($driver === 'mysql') { $charset = $this->hasCap( 'utf8mb4' ) ? 'utf8mb4' : 'utf8'; $collate = $this->hasCap( 'utf8mb4_520' ) ? '_unicode_520_ci' : '_unicode_ci'; $this->pdo->setAttribute(\PDO::MYSQL_ATTR_INIT_COMMAND, 'SET NAMES '. $charset ); //on every re-connect $this->pdo->exec(' SET NAMES '. $charset); //also for current connection $this->mysqlCharset = $charset; $this->mysqlCollate = $charset . $collate; } } /** * Determine if a database supports a particular feature. * * @param $db_cap identifier of database capability * * @return int|false Whether the database feature is supported, false otherwise. **/ protected function hasCap( $db_cap ) { $version = $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ); switch ( strtolower( $db_cap ) ) { case 'utf8mb4': if ( version_compare( $version, '5.5.3', '<' ) ) { return false; } $client_version = $this->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION ); /* * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server. * mysqlnd has supported utf8mb4 since 5.0.9. */ if ( false !== strpos( $client_version, 'mysqlnd' ) ) { $client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version ); return version_compare( $client_version, '5.0.9', '>=' ); } else { return version_compare( $client_version, '5.5.3', '>=' ); } break; case 'utf8mb4_520': return version_compare( $version, '5.6', '>=' ); break; } return false; } /** * Constructor. You may either specify dsn, user and password or * just give an existing PDO connection. * * Examples: * $driver = new RPDO($dsn, $user, $password); * $driver = new RPDO($existingConnection); * * @param string|object $dsn database connection string * @param string $user optional, usename to sign in * @param string $pass optional, password for connection login * * @return void */ public function __construct( $dsn, $user = NULL, $pass = NULL ) { if ( is_object( $dsn ) ) { $this->pdo = $dsn; $this->isConnected = TRUE; $this->setEncoding(); $this->pdo->setAttribute( \PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION ); $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE,\PDO::FETCH_ASSOC ); // make sure that the dsn at least contains the type $this->dsn = $this->getDatabaseType(); } else { $this->dsn = $dsn; $this->connectInfo = array( 'pass' => $pass, 'user' => $user ); } //PHP 5.3 PDO SQLite has a bug with large numbers: if ( ( strpos( $this->dsn, 'sqlite' ) === 0 && PHP_MAJOR_VERSION === 5 && PHP_MINOR_VERSION === 3 ) || defined('HHVM_VERSION') || $this->dsn === 'test-sqlite-53' ) { $this->max = 2147483647; //otherwise you get -2147483648 ?! demonstrated in build #603 on Travis. } elseif ( strpos( $this->dsn, 'cubrid' ) === 0 ) { $this->max = 2147483647; //bindParam in pdo_cubrid also fails... } else { $this->max = PHP_INT_MAX; //the normal value of course (makes it possible to use large numbers in LIMIT clause) } } /** * Sets PDO in stringify fetch mode. * * @param boolean $bool */ public function stringifyFetches( $bool ) { $this->stringifyFetches = $bool; } /** * Returns the best possible encoding for MySQL based on version data. * * @param boolean $retCol pass TRUE to return both charset/collate * * @return string|array */ public function getMysqlEncoding( $retCol = FALSE ) { if( $retCol ) return array( 'charset' => $this->mysqlCharset, 'collate' => $this->mysqlCollate ); return $this->mysqlCharset; } /** * Whether to bind all parameters as strings. * If set to TRUE this will cause all integers to be bound as STRINGS. * This will NOT affect NULL values. * * @param boolean $yesNo pass TRUE to bind all parameters as strings. * * @return void */ public function setUseStringOnlyBinding( $yesNo ) { $this->flagUseStringOnlyBinding = (boolean) $yesNo; } /** * Sets the maximum value to be bound as integer, normally * this value equals PHP's MAX INT constant, however sometimes * PDO driver bindings cannot bind large integers as integers. * This method allows you to manually set the max integer binding * value to manage portability/compatibility issues among different * PHP builds. This method will return the old value. * * @param integer $max maximum value for integer bindings * * @return integer */ public function setMaxIntBind( $max ) { if ( !is_integer( $max ) ) throw new RedException( 'Parameter has to be integer.' ); $oldMax = $this->max; $this->max = $max; return $oldMax; } /** * Establishes a connection to the database using PHP\PDO * functionality. If a connection has already been established this * method will simply return directly. This method also turns on * UTF8 for the database and PDO-ERRMODE-EXCEPTION as well as * PDO-FETCH-ASSOC. * * @return void */ public function connect() { if ( $this->isConnected ) return; try { $user = $this->connectInfo['user']; $pass = $this->connectInfo['pass']; $this->pdo = new \PDO( $this->dsn, $user, $pass ); $this->setEncoding(); $this->pdo->setAttribute( \PDO::ATTR_STRINGIFY_FETCHES, $this->stringifyFetches ); //cant pass these as argument to constructor, CUBRID driver does not understand... $this->pdo->setAttribute( \PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION ); $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC ); $this->isConnected = TRUE; /* run initialisation query if any */ if ( $this->initSQL !== NULL ) { $this->Execute( $this->initSQL ); $this->initSQL = NULL; } } catch ( \PDOException $exception ) { $matches = array(); $dbname = ( preg_match( '/dbname=(\w+)/', $this->dsn, $matches ) ) ? $matches[1] : '?'; throw new \PDOException( 'Could not connect to database (' . $dbname . ').', $exception->getCode() ); } } /** * Directly sets PDO instance into driver. * This method might improve performance, however since the driver does * not configure this instance terrible things may happen... only use * this method if you are an expert on RedBeanPHP, PDO and UTF8 connections and * you know your database server VERY WELL. * * @param PDO $pdo PDO instance * * @return void */ public function setPDO( \PDO $pdo ) { $this->pdo = $pdo; } /** * @see Driver::GetAll */ public function GetAll( $sql, $bindings = array() ) { $this->runQuery( $sql, $bindings ); return $this->resultArray; } /** * @see Driver::GetAssocRow */ public function GetAssocRow( $sql, $bindings = array() ) { $this->runQuery( $sql, $bindings, array( 'fetchStyle' => \PDO::FETCH_ASSOC ) ); return $this->resultArray; } /** * @see Driver::GetCol */ public function GetCol( $sql, $bindings = array() ) { $rows = $this->GetAll( $sql, $bindings ); $cols = array(); if ( $rows && is_array( $rows ) && count( $rows ) > 0 ) { foreach ( $rows as $row ) { $cols[] = array_shift( $row ); } } return $cols; } /** * @see Driver::GetOne */ public function GetOne( $sql, $bindings = array() ) { $arr = $this->GetAll( $sql, $bindings ); $res = NULL; if ( !is_array( $arr ) ) return NULL; if ( count( $arr ) === 0 ) return NULL; $row1 = array_shift( $arr ); if ( !is_array( $row1 ) ) return NULL; if ( count( $row1 ) === 0 ) return NULL; $col1 = array_shift( $row1 ); return $col1; } /** * Alias for getOne(). * Backward compatibility. * * @param string $sql SQL * @param array $bindings bindings * * @return mixed */ public function GetCell( $sql, $bindings = array() ) { return $this->GetOne( $sql, $bindings ); } /** * @see Driver::GetRow */ public function GetRow( $sql, $bindings = array() ) { $arr = $this->GetAll( $sql, $bindings ); return array_shift( $arr ); } /** * @see Driver::Excecute */ public function Execute( $sql, $bindings = array() ) { $this->runQuery( $sql, $bindings ); return $this->affectedRows; } /** * @see Driver::GetInsertID */ public function GetInsertID() { $this->connect(); return (int) $this->pdo->lastInsertId(); } /** * @see Driver::GetCursor */ public function GetCursor( $sql, $bindings = array() ) { $statement = $this->runQuery( $sql, $bindings, array( 'noFetch' => TRUE ) ); $cursor = new PDOCursor( $statement, \PDO::FETCH_ASSOC ); return $cursor; } /** * @see Driver::Affected_Rows */ public function Affected_Rows() { $this->connect(); return (int) $this->affectedRows; } /** * Toggles debug mode. In debug mode the driver will print all * SQL to the screen together with some information about the * results. * * @param boolean $trueFalse turn on/off * @param Logger $logger logger instance * * @return void */ public function setDebugMode( $tf, $logger = NULL ) { $this->connect(); $this->loggingEnabled = (bool) $tf; if ( $this->loggingEnabled and !$logger ) { $logger = new RDefault(); } $this->setLogger( $logger ); } /** * Injects Logger object. * Sets the logger instance you wish to use. * * @param Logger $logger the logger instance to be used for logging * * @return self */ public function setLogger( Logger $logger ) { $this->logger = $logger; return $this; } /** * Gets Logger object. * Returns the currently active Logger instance. * * @return Logger */ public function getLogger() { return $this->logger; } /** * @see Driver::StartTrans */ public function StartTrans() { $this->connect(); $this->pdo->beginTransaction(); } /** * @see Driver::CommitTrans */ public function CommitTrans() { $this->connect(); $this->pdo->commit(); } /** * @see Driver::FailTrans */ public function FailTrans() { $this->connect(); $this->pdo->rollback(); } /** * Returns the name of database driver for PDO. * Uses the PDO attribute DRIVER NAME to obtain the name of the * PDO driver. * * @return string */ public function getDatabaseType() { $this->connect(); return $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME ); } /** * Returns the version number of the database. * * @return mixed */ public function getDatabaseVersion() { $this->connect(); return $this->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION ); } /** * Returns the underlying PHP PDO instance. * * @return PDO */ public function getPDO() { $this->connect(); return $this->pdo; } /** * Closes database connection by destructing PDO. * * @return void */ public function close() { $this->pdo = NULL; $this->isConnected = FALSE; } /** * Returns TRUE if the current PDO instance is connected. * * @return boolean */ public function isConnected() { return $this->isConnected && $this->pdo; } /** * Toggles logging, enables or disables logging. * * @param boolean $enable TRUE to enable logging * * @return self */ public function setEnableLogging( $enable ) { $this->loggingEnabled = (boolean) $enable; return $this; } /** * Resets the internal Query Counter. * * @return self */ public function resetCounter() { $this->queryCounter = 0; return $this; } /** * Returns the number of SQL queries processed. * * @return integer */ public function getQueryCount() { return $this->queryCounter; } /** * Returns the maximum value treated as integer parameter * binding. * * This method is mainly for testing purposes but it can help * you solve some issues relating to integer bindings. * * @return integer */ public function getIntegerBindingMax() { return $this->max; } /** * Sets a query to be executed upon connecting to the database. * This method provides an opportunity to configure the connection * to a database through an SQL-based interface. Objects can provide * an SQL string to be executed upon establishing a connection to * the database. This has been used to solve issues with default * foreign key settings in SQLite3 for instance, see Github issues: * #545 and #548. * * @param string $sql SQL query to run upon connecting to database * * @return self */ public function setInitQuery( $sql ) { $this->initSQL = $sql; return $this; } } } namespace RedBeanPHP { use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; use RedBeanPHP\BeanHelper as BeanHelper; use RedBeanPHP\RedException as RedException; /* PHP 5.3 compatibility */ if (interface_exists('\JsonSerializable')) { /* We extend JsonSerializable to avoid namespace conflicts, can't define interface with special namespace in PHP */ interface Jsonable extends \JsonSerializable {}; } else { interface Jsonable {}; } /** * OODBBean (Object Oriented DataBase Bean). * * to exchange information with the database. A bean represents * a single table row and offers generic services for interaction * with databases systems as well as some meta-data. * * @file RedBeanPHP/OODBBean.php * @author Gabor de Mooij and the RedBeanPHP community * @license BSD/GPLv2 * @desc OODBBean represents a bean. RedBeanPHP uses beans * * @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. */ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable,Jsonable { /** * FUSE error modes. */ const C_ERR_IGNORE = FALSE; const C_ERR_LOG = 1; const C_ERR_NOTICE = 2; const C_ERR_WARN = 3; const C_ERR_EXCEPTION = 4; const C_ERR_FUNC = 5; const C_ERR_FATAL = 6; /** * @var boolean */ protected static $convertArraysToJSON = FALSE; /** * @var boolean */ protected static $errorHandlingFUSE = FALSE; /** * @var callable|NULL */ protected static $errorHandler = NULL; /** * @var array */ protected static $aliases = array(); /** * @var boolean */ protected static $autoResolve = FALSE; /** * If this is set to TRUE, the __toString function will * encode all properties as UTF-8 to repair invalid UTF-8 * encodings and prevent exceptions (which are uncatchable from within * a __toString-function). * * @var boolean */ protected static $enforceUTF8encoding = FALSE; /** * This is where the real properties of the bean live. They are stored and retrieved * by the magic getter and setter (__get and __set). * * @var array $properties */ protected $properties = array(); /** * Here we keep the meta data of a bean. * * @var array */ protected $__info = array(); /** * The BeanHelper allows the bean to access the toolbox objects to implement * rich functionality, otherwise you would have to do everything with R or * external objects. * * @var BeanHelper */ protected $beanHelper = NULL; /** * @var null */ protected $fetchType = NULL; /** * @var string */ protected $withSql = ''; /** * @var array */ protected $withParams = array(); /** * @var string */ protected $aliasName = NULL; /** * @var string */ protected $via = NULL; /** * @var boolean */ protected $noLoad = FALSE; /** * @var boolean */ protected $all = FALSE; /** * If this is set to TRUE, the __toString function will * encode all properties as UTF-8 to repair invalid UTF-8 * encodings and prevent exceptions (which are uncatchable from within * a __toString-function). * * @param boolean $toggle TRUE to enforce UTF-8 encoding (slower) * * @return void */ public static function setEnforceUTF8encoding( $toggle ) { self::$enforceUTF8encoding = (boolean) $toggle; } /** * Sets the error mode for FUSE. * What to do if a FUSE model method does not exist? * You can set the following options: * * * OODBBean::C_ERR_IGNORE (default), ignores the call, returns NULL * * OODBBean::C_ERR_LOG, logs the incident using error_log * * OODBBean::C_ERR_NOTICE, triggers a E_USER_NOTICE * * OODBBean::C_ERR_WARN, triggers a E_USER_WARNING * * OODBBean::C_ERR_EXCEPTION, throws an exception * * OODBBean::C_ERR_FUNC, allows you to specify a custom handler (function) * * OODBBean::C_ERR_FATAL, triggers a E_USER_ERROR * * * Custom handler method signature: handler( array ( * 'message' => string * 'bean' => OODBBean * 'method' => string * ) ) * * * This method returns the old mode and handler as an array. * * @param integer $mode error handling mode * @param callable|NULL $func custom handler * * @return array */ public static function setErrorHandlingFUSE($mode, $func = NULL) { if ( $mode !== self::C_ERR_IGNORE && $mode !== self::C_ERR_LOG && $mode !== self::C_ERR_NOTICE && $mode !== self::C_ERR_WARN && $mode !== self::C_ERR_EXCEPTION && $mode !== self::C_ERR_FUNC && $mode !== self::C_ERR_FATAL ) throw new \Exception( 'Invalid error mode selected' ); if ( $mode === self::C_ERR_FUNC && !is_callable( $func ) ) { throw new \Exception( 'Invalid error handler' ); } $old = array( self::$errorHandlingFUSE, self::$errorHandler ); self::$errorHandlingFUSE = $mode; if ( is_callable( $func ) ) { self::$errorHandler = $func; } else { self::$errorHandler = NULL; } return $old; } /** * Toggles array to JSON conversion. If set to TRUE any array * set to a bean property that's not a list will be turned into * a JSON string. Used together with AQueryWriter::useJSONColumns this * extends the data type support for JSON columns. Returns the previous * value of the flag. * * @param boolean $flag flag * * @return boolean */ public static function convertArraysToJSON( $flag ) { $old = self::$convertArraysToJSON; self::$convertArraysToJSON = $flag; return $old; } /** * Sets global aliases. * Registers a batch of aliases in one go. This works the same as * fetchAs and setAutoResolve but explicitly. For instance if you register * the alias 'cover' for 'page' a property containing a reference to a * page bean called 'cover' will correctly return the page bean and not * a (non-existant) cover bean. * * * R::aliases( array( 'cover' => 'page' ) ); * $book = R::dispense( 'book' ); * $page = R::dispense( 'page' ); * $book->cover = $page; * R::store( $book ); * $book = $book->fresh(); * $cover = $book->cover; * echo $cover->getMeta( 'type' ); //page * * * The format of the aliases registration array is: * * {alias} => {actual type} * * In the example above we use: * * cover => page * * From that point on, every bean reference to a cover * will return a 'page' bean. Note that with autoResolve this * feature along with fetchAs() is no longer very important, although * relying on explicit aliases can be a bit faster. * * @param array $list list of global aliases to use * * @return void */ public static function aliases( $list ) { self::$aliases = $list; } /** * Enables or disables auto-resolving fetch types. * Auto-resolving aliased parent beans is convenient but can * be slower and can create infinite recursion if you * used aliases to break cyclic relations in your domain. * * @param boolean $automatic TRUE to enable automatic resolving aliased parents * * @return void */ public static function setAutoResolve( $automatic = TRUE ) { self::$autoResolve = (boolean) $automatic; } /** * Sets a meta property for all beans. This is a quicker way to set * the meta properties for a collection of beans because this method * can directly access the property arrays of the beans. * This method returns the beans. * * @param array $beans beans to set the meta property of * @param string $property property to set * @param mixed $value value * * @return array */ public static function setMetaAll( $beans, $property, $value ) { foreach( $beans as $bean ) { if ( $bean instanceof OODBBean ) $bean->__info[ $property ] = $value; } return $beans; } /** * Parses the join in the with-snippet. * For instance: * * * $author * ->withCondition(' @joined.detail.title LIKE ? ') * ->ownBookList; * * * will automatically join 'detail' on book to * access the title field. * * @note this feature requires Narrow Field Mode and Join Feature * to be both activated (default). * * @param string $type the source type for the join * * @return string */ private function parseJoin( $type ) { $joinSql = ''; $joins = array(); if ( strpos($this->withSql, '@joined.' ) !== FALSE ) { $writer = $this->beanHelper->getToolBox()->getWriter(); $oldParts = $parts = explode( '@joined.', $this->withSql ); array_shift( $parts ); foreach($parts as $part) { $explosion = explode( '.', $part ); $joinInfo = array_shift( $explosion ); //Dont join more than once.. if ( !isset( $joins[$joinInfo] ) ) { $joins[ $joinInfo ] = true; $joinSql .= $writer->writeJoin( $type, $joinInfo, 'LEFT' ); } } $this->withSql = implode( '', $oldParts ); $joinSql .= ' WHERE '; } return $joinSql; } /** * Internal method. * Obtains a shared list for a certain type. * * @param string $type the name of the list you want to retrieve. * * @return array */ private function getSharedList( $type, $redbean, $toolbox ) { $writer = $toolbox->getWriter(); if ( $this->via ) { $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) ); if ( $oldName !== $this->via ) { //set the new renaming rule $writer->renameAssocTable( $oldName, $this->via ); } $this->via = NULL; } $beans = array(); if ($this->getID()) { $type = $this->beau( $type ); $assocManager = $redbean->getAssociationManager(); $beans = $assocManager->related( $this, $type, $this->withSql, $this->withParams ); } $this->withSql = ''; $this->withParams = array(); return $beans; } /** * Internal method. * Obtains the own list of a certain type. * * @param string $type name of the list you want to retrieve * @param OODB $oodb The RB OODB object database instance * * @return array */ private function getOwnList( $type, $redbean ) { $type = $this->beau( $type ); if ( $this->aliasName ) { $parentField = $this->aliasName; $myFieldLink = $parentField . '_id'; $this->__info['sys.alias.' . $type] = $this->aliasName; $this->aliasName = NULL; } else { $parentField = $this->__info['type']; $myFieldLink = $parentField . '_id'; } $beans = array(); if ( $this->getID() ) { $firstKey = NULL; if ( count( $this->withParams ) > 0 ) { reset( $this->withParams ); $firstKey = key( $this->withParams ); } $joinSql = $this->parseJoin( $type ); if ( !is_numeric( $firstKey ) || $firstKey === NULL ) { $bindings = $this->withParams; $bindings[':slot0'] = $this->getID(); $beans = $redbean->find( $type, array(), " {$joinSql} $myFieldLink = :slot0 " . $this->withSql, $bindings ); } else { $bindings = array_merge( array( $this->getID() ), $this->withParams ); $beans = $redbean->find( $type, array(), " {$joinSql} $myFieldLink = ? " . $this->withSql, $bindings ); } } $this->withSql = ''; $this->withParams = array(); foreach ( $beans as $beanFromList ) { $beanFromList->__info['sys.parentcache.' . $parentField] = $this; } return $beans; } /** * Initializes a bean. Used by OODB for dispensing beans. * It is not recommended to use this method to initialize beans. Instead * use the OODB object to dispense new beans. You can use this method * if you build your own bean dispensing mechanism. * * @param string $type type of the new bean * @param BeanHelper $beanhelper bean helper to obtain a toolbox and a model * * @return void */ public function initializeForDispense( $type, BeanHelper $beanhelper ) { $this->beanHelper = $beanhelper; $this->__info['type'] = $type; $this->__info['sys.id'] = 'id'; $this->__info['sys.orig'] = array( 'id' => 0 ); $this->__info['tainted'] = TRUE; $this->__info['changed'] = TRUE; $this->__info['changelist'] = array(); $this->properties['id'] = 0; } /** * Sets the Bean Helper. Normally the Bean Helper is set by OODB. * Here you can change the Bean Helper. The Bean Helper is an object * providing access to a toolbox for the bean necessary to retrieve * nested beans (bean lists: ownBean, sharedBean) without the need to * rely on static calls to the facade (or make this class dep. on OODB). * * @param BeanHelper $helper helper to use for this bean * * @return void */ public function setBeanHelper( BeanHelper $helper ) { $this->beanHelper = $helper; } /** * Returns an ArrayIterator so you can treat the bean like * an array with the properties container as its contents. * This method is meant for PHP and allows you to access beans as if * they were arrays, i.e. using array notation: * * $bean[$key] = $value; * * Note that not all PHP functions work with the array interface. * * @return ArrayIterator */ public function getIterator() { return new \ArrayIterator( $this->properties ); } /** * Imports all values from an associative array $array. Chainable. * This method imports the values in the first argument as bean * propery and value pairs. Use the second parameter to provide a * selection. If a selection array is passed, only the entries * having keys mentioned in the selection array will be imported. * Set the third parameter to TRUE to preserve spaces in selection keys. * * @param array $array what you want to import * @param string|array $selection selection of values * @param boolean $notrim if TRUE selection keys will NOT be trimmed * * @return OODBBean */ public function import( $array, $selection = FALSE, $notrim = FALSE ) { if ( is_string( $selection ) ) { $selection = explode( ',', $selection ); } if ( !$notrim && is_array( $selection ) ) { foreach ( $selection as $key => $selected ) { $selection[$key] = trim( $selected ); } } foreach ( $array as $key => $value ) { if ( $key != '__info' ) { if ( !$selection || ( $selection && in_array( $key, $selection ) ) ) { if ( is_array($value ) ) { if ( isset( $value['_type'] ) ) { $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $value['_type'] ); unset( $value['_type'] ); $bean->import($value); $this->$key = $bean; } else { $listBeans = array(); foreach( $value as $listKey => $listItem ) { $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $listItem['_type'] ); unset( $listItem['_type'] ); $bean->import($listItem); $list = &$this->$key; $list[ $listKey ] = $bean; } } } else { $this->$key = $value; } } } } return $this; } /** * Fast way to import a row. * Does not perform any checks. * * @param array $row a database row * * @return self */ public function importRow( $row ) { $this->properties = $row; $this->__info['sys.orig'] = $row; $this->__info['changed'] = FALSE; return $this; } /** * Imports data from another bean. Chainable. * Copies the properties from the source bean to the internal * property list. * * @param OODBBean $sourceBean the source bean to take properties from * * @return OODBBean */ public function importFrom( OODBBean $sourceBean ) { $this->__info['tainted'] = TRUE; $this->__info['changed'] = TRUE; $this->properties = $sourceBean->properties; return $this; } /** * Injects the properties of another bean but keeps the original ID. * Just like import() but keeps the original ID. * Chainable. * * @param OODBBean $otherBean the bean whose properties you would like to copy * * @return OODBBean */ public function inject( OODBBean $otherBean ) { $myID = $this->properties['id']; $this->import( $otherBean->export( FALSE, FALSE, TRUE ) ); $this->id = $myID; return $this; } /** * Exports the bean as an array. * This function exports the contents of a bean to an array and returns * the resulting array. * * @param boolean $meta set to TRUE if you want to export meta data as well * @param boolean $parents set to TRUE if you want to export parents as well * @param boolean $onlyMe set to TRUE if you want to export only this bean * @param array $filters optional whitelist for export * * @return array */ public function export( $meta = FALSE, $parents = FALSE, $onlyMe = FALSE, $filters = array() ) { $arr = array(); if ( $parents ) { foreach ( $this as $key => $value ) { if ( substr( $key, -3 ) != '_id' ) continue; $prop = substr( $key, 0, strlen( $key ) - 3 ); $this->$prop; } } $hasFilters = is_array( $filters ) && count( $filters ); foreach ( $this as $key => $value ) { if ( !$onlyMe && is_array( $value ) ) { $vn = array(); foreach ( $value as $i => $b ) { if ( !( $b instanceof OODBBean ) ) continue; $vn[] = $b->export( $meta, FALSE, FALSE, $filters ); $value = $vn; } } elseif ( $value instanceof OODBBean ) { if ( $hasFilters ) { if ( !in_array( strtolower( $value->getMeta( 'type' ) ), $filters ) ) continue; } $value = $value->export( $meta, $parents, FALSE, $filters ); } $arr[$key] = $value; } if ( $meta ) { $arr['__info'] = $this->__info; } return $arr; } /** * Implements isset() function for use as an array. * * @param string $property name of the property you want to check * * @return boolean */ public function __isset( $property ) { $property = $this->beau( $property ); if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { $property = substr($property, 1); } return isset( $this->properties[$property] ); } /** * Checks whether a related bean exists. * For instance if a post bean has a related author, this method * can be used to check if the author is set without loading the author. * This method works by checking the related ID-field. * * @param string $property name of the property you wish to check * * @return boolean */ public function exists( $property ) { $property = $this->beau( $property ); /* fixes issue #549, see Base/Bean test */ $hiddenRelationField = "{$property}_id"; if ( array_key_exists( $hiddenRelationField, $this->properties ) ) { if ( !is_null( $this->properties[$hiddenRelationField] ) ) { return TRUE; } } return FALSE; } /** * Returns the ID of the bean no matter what the ID field is. * * @return string|null */ public function getID() { return ( isset( $this->properties['id'] ) ) ? (string) $this->properties['id'] : NULL; } /** * Unsets a property of a bean. * Magic method, gets called implicitly when performing the unset() operation * on a bean property. * * @param string $property property to unset * * @return void */ public function __unset( $property ) { $property = $this->beau( $property ); if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { $property = substr($property, 1); } unset( $this->properties[$property] ); $shadowKey = 'sys.shadow.'.$property; if ( isset( $this->__info[ $shadowKey ] ) ) unset( $this->__info[$shadowKey] ); //also clear modifiers $this->withSql = ''; $this->withParams = array(); $this->aliasName = NULL; $this->fetchType = NULL; $this->noLoad = FALSE; $this->all = FALSE; $this->via = NULL; return; } /** * Adds WHERE clause conditions to ownList retrieval. * For instance to get the pages that belong to a book you would * issue the following command: $book->ownPage * However, to order these pages by number use: * * * $book->with(' ORDER BY `number` ASC ')->ownPage * * * the additional SQL snippet will be merged into the final * query. * * @param string $sql SQL to be added to retrieval query. * @param array $bindings array with parameters to bind to SQL snippet * * @return OODBBean */ public function with( $sql, $bindings = array() ) { $this->withSql = $sql; $this->withParams = $bindings; return $this; } /** * Just like with(). Except that this method prepends the SQL query snippet * with AND which makes it slightly more comfortable to use a conditional * SQL snippet. For instance to filter an own-list with pages (belonging to * a book) on specific chapters you can use: * * $book->withCondition(' chapter = 3 ')->ownPage * * This will return in the own list only the pages having 'chapter == 3'. * * @param string $sql SQL to be added to retrieval query (prefixed by AND) * @param array $bindings array with parameters to bind to SQL snippet * * @return OODBBean */ public function withCondition( $sql, $bindings = array() ) { $this->withSql = ' AND ' . $sql; $this->withParams = $bindings; return $this; } /** * Tells the bean to (re)load the following list without any * conditions. If you have an ownList or sharedList with a * condition you can use this method to reload the entire list. * * Usage: * * * $bean->with( ' LIMIT 3 ' )->ownPage; //Just 3 * $bean->all()->ownPage; //Reload all pages * * * @return self */ public function all() { $this->all = TRUE; return $this; } /** * Tells the bean to only access the list but not load * its contents. Use this if you only want to add something to a list * and you have no interest in retrieving its contents from the database. * * @return self */ public function noLoad() { $this->noLoad = TRUE; return $this; } /** * Prepares an own-list to use an alias. This is best explained using * an example. Imagine a project and a person. The project always involves * two persons: a teacher and a student. The person beans have been aliased in this * case, so to the project has a teacher_id pointing to a person, and a student_id * also pointing to a person. Given a project, we obtain the teacher like this: * * * $project->fetchAs('person')->teacher; * * * Now, if we want all projects of a teacher we cant say: * * * $teacher->ownProject * * * because the $teacher is a bean of type 'person' and no project has been * assigned to a person. Instead we use the alias() method like this: * * * $teacher->alias('teacher')->ownProject * * * now we get the projects associated with the person bean aliased as * a teacher. * * @param string $aliasName the alias name to use * * @return OODBBean */ public function alias( $aliasName ) { $this->aliasName = $this->beau( $aliasName ); return $this; } /** * Returns properties of bean as an array. * This method returns the raw internal property list of the * bean. Only use this method for optimization purposes. Otherwise * use the export() method to export bean data to arrays. * * @return array */ public function getProperties() { return $this->properties; } /** * Returns properties of bean as an array. * This method returns the raw internal property list of the * bean. Only use this method for optimization purposes. Otherwise * use the export() method to export bean data to arrays. * This method returns an array with the properties array and * the type (string). * * @return array */ public function getPropertiesAndType() { return array( $this->properties, $this->__info['type'] ); } /** * Turns a camelcase property name into an underscored property name. * * Examples: * * * oneACLRoute -> one_acl_route * * camelCase -> camel_case * * Also caches the result to improve performance. * * @param string $property property to un-beautify * * @return string */ public function beau( $property ) { static $beautifulColumns = array(); if ( ctype_lower( $property ) ) return $property; if ( ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) || ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) || ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) ) { $property = preg_replace( '/List$/', '', $property ); return $property; } if ( !isset( $beautifulColumns[$property] ) ) { $beautifulColumns[$property] = AQueryWriter::camelsSnake( $property ); } return $beautifulColumns[$property]; } /** * Clears all modifiers. * * @return self */ public function clearModifiers() { $this->withSql = ''; $this->withParams = array(); $this->aliasName = NULL; $this->fetchType = NULL; $this->noLoad = FALSE; $this->all = FALSE; $this->via = NULL; return $this; } /** * Determines whether a list is opened in exclusive mode or not. * If a list has been opened in exclusive mode this method will return TRUE, * othwerwise it will return FALSE. * * @param string $listName name of the list to check * * @return boolean */ public function isListInExclusiveMode( $listName ) { $listName = $this->beau( $listName ); if ( strpos( $listName, 'xown' ) === 0 && ctype_upper( substr( $listName, 4, 1 ) ) ) { $listName = substr($listName, 1); } $listName = lcfirst( substr( $listName, 3 ) ); return ( isset( $this->__info['sys.exclusive-'.$listName] ) && $this->__info['sys.exclusive-'.$listName] ); } /** * Magic Getter. Gets the value for a specific property in the bean. * If the property does not exist this getter will make sure no error * occurs. This is because RedBean allows you to query (probe) for * properties. If the property can not be found this method will * return NULL instead. * * @param string $property name of the property you wish to obtain the value of * * @return mixed */ public function &__get( $property ) { $isEx = FALSE; $isOwn = FALSE; $isShared = FALSE; if ( !ctype_lower( $property ) ) { $property = $this->beau( $property ); if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { $property = substr($property, 1); $listName = lcfirst( substr( $property, 3 ) ); $isEx = TRUE; $isOwn = TRUE; $this->__info['sys.exclusive-'.$listName] = TRUE; } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) { $isOwn = TRUE; $listName = lcfirst( substr( $property, 3 ) ); } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) { $isShared = TRUE; } } $fieldLink = $property . '_id'; $exists = isset( $this->properties[$property] ); //If not exists and no field link and no list, bail out. if ( !$exists && !isset($this->$fieldLink) && (!$isOwn && !$isShared )) { $this->withSql = ''; $this->withParams = array(); $this->aliasName = NULL; $this->fetchType = NULL; $this->noLoad = FALSE; $this->all = FALSE; $this->via = NULL; $NULL = NULL; return $NULL; } $hasAlias = (!is_null($this->aliasName)); $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ? ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE; $hasSQL = ($this->withSql !== '' || $this->via !== NULL); $hasAll = (boolean) ($this->all); //If exists and no list or exits and list not changed, bail out. if ( $exists && ((!$isOwn && !$isShared ) || (!$hasSQL && !$differentAlias && !$hasAll)) ) { $this->withSql = ''; $this->withParams = array(); $this->aliasName = NULL; $this->fetchType = NULL; $this->noLoad = FALSE; $this->all = FALSE; $this->via = NULL; return $this->properties[$property]; } list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox(); if ( isset( $this->$fieldLink ) ) { $this->__info['tainted'] = TRUE; if ( isset( $this->__info["sys.parentcache.$property"] ) ) { $bean = $this->__info["sys.parentcache.$property"]; } else { if ( isset( self::$aliases[$property] ) ) { $type = self::$aliases[$property]; } elseif ( $this->fetchType ) { $type = $this->fetchType; $this->fetchType = NULL; } else { $type = $property; } $bean = NULL; if ( !is_null( $this->properties[$fieldLink] ) ) { $bean = $redbean->load( $type, $this->properties[$fieldLink] ); //If the IDs dont match, we failed to load, so try autoresolv in that case... if ( $bean->id !== $this->properties[$fieldLink] && self::$autoResolve ) { $type = $this->beanHelper->getToolbox()->getWriter()->inferFetchType( $this->__info['type'], $property ); if ( !is_null( $type) ) { $bean = $redbean->load( $type, $this->properties[$fieldLink] ); $this->__info["sys.autoresolved.{$property}"] = $type; } } } } $this->properties[$property] = $bean; $this->withSql = ''; $this->withParams = array(); $this->aliasName = NULL; $this->fetchType = NULL; $this->noLoad = FALSE; $this->all = FALSE; $this->via = NULL; return $this->properties[$property]; } //Implicit: elseif ( $isOwn || $isShared ) { if ( $this->noLoad ) { $beans = array(); } elseif ( $isOwn ) { $beans = $this->getOwnList( $listName, $redbean ); } else { $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox ); } $this->properties[$property] = $beans; $this->__info["sys.shadow.$property"] = $beans; $this->__info['tainted'] = TRUE; $this->withSql = ''; $this->withParams = array(); $this->aliasName = NULL; $this->fetchType = NULL; $this->noLoad = FALSE; $this->all = FALSE; $this->via = NULL; return $this->properties[$property]; } /** * Magic Setter. Sets the value for a specific property. * This setter acts as a hook for OODB to mark beans as tainted. * The tainted meta property can be retrieved using getMeta("tainted"). * The tainted meta property indicates whether a bean has been modified and * can be used in various caching mechanisms. * * @param string $property name of the property you wish to assign a value to * @param mixed $value the value you want to assign * * @return void */ public function __set( $property, $value ) { $isEx = FALSE; $isOwn = FALSE; $isShared = FALSE; if ( !ctype_lower( $property ) ) { $property = $this->beau( $property ); if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { $property = substr($property, 1); $listName = lcfirst( substr( $property, 3 ) ); $isEx = TRUE; $isOwn = TRUE; $this->__info['sys.exclusive-'.$listName] = TRUE; } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) { $isOwn = TRUE; $listName = lcfirst( substr( $property, 3 ) ); } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) { $isShared = TRUE; } } elseif ( self::$convertArraysToJSON && is_array( $value ) ) { $value = json_encode( $value ); } $hasAlias = (!is_null($this->aliasName)); $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ? ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE; $hasSQL = ($this->withSql !== '' || $this->via !== NULL); $exists = isset( $this->properties[$property] ); $fieldLink = $property . '_id'; $isFieldLink = (($pos = strrpos($property, '_id')) !== FALSE) && array_key_exists( ($fieldName = substr($property, 0, $pos)), $this->properties ); if ( ($isOwn || $isShared) && (!$exists || $hasSQL || $differentAlias) ) { if ( !$this->noLoad ) { list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox(); if ( $isOwn ) { $beans = $this->getOwnList( $listName, $redbean ); } else { $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox ); } $this->__info["sys.shadow.$property"] = $beans; } } $this->withSql = ''; $this->withParams = array(); $this->aliasName = NULL; $this->fetchType = NULL; $this->noLoad = FALSE; $this->all = FALSE; $this->via = NULL; $this->__info['tainted'] = TRUE; $this->__info['changed'] = TRUE; array_push( $this->__info['changelist'], $property ); if ( array_key_exists( $fieldLink, $this->properties ) && !( $value instanceof OODBBean ) ) { if ( is_null( $value ) || $value === FALSE ) { unset( $this->properties[ $property ]); $this->properties[ $fieldLink ] = NULL; return; } else { throw new RedException( 'Cannot cast to bean.' ); } } if ( $isFieldLink ){ unset( $this->properties[ $fieldName ]); $this->properties[ $property ] = NULL; } if ( $value === FALSE ) { $value = '0'; } elseif ( $value === TRUE ) { $value = '1'; } elseif ( $value instanceof \DateTime ) { $value = $value->format( 'Y-m-d H:i:s' ); } $this->properties[$property] = $value; } /** * Sets a property directly, for internal use only. * * @param string $property property * @param mixed $value value * @param boolean $updateShadow whether you want to update the shadow * @param boolean $taint whether you want to mark the bean as tainted * * @return void */ public function setProperty( $property, $value, $updateShadow = FALSE, $taint = FALSE ) { $this->properties[$property] = $value; if ( $updateShadow ) { $this->__info['sys.shadow.' . $property] = $value; } if ( $taint ) { $this->__info['tainted'] = TRUE; $this->__info['changed'] = TRUE; } } /** * Returns the value of a meta property. A meta property * contains additional information about the bean object that will not * be stored in the database. Meta information is used to instruct * RedBeanPHP as well as other systems how to deal with the bean. * If the property cannot be found this getter will return NULL instead. * * Example: * * * $bean->setMeta( 'flush-cache', TRUE ); * * * RedBeanPHP also stores meta data in beans, this meta data uses * keys prefixed with 'sys.' (system). * * @param string $path path to property in meta data * @param mixed $default default value * * @return mixed */ public function getMeta( $path, $default = NULL ) { return ( isset( $this->__info[$path] ) ) ? $this->__info[$path] : $default; } /** * Gets and unsets a meta property. * Moves a meta property out of the bean. * This is a short-cut method that can be used instead * of combining a get/unset. * * @param string $path path to property in meta data * @param mixed $default default value * * @return mixed */ public function moveMeta( $path, $value = NULL ) { if ( isset( $this->__info[$path] ) ) { $value = $this->__info[ $path ]; unset( $this->__info[ $path ] ); } return $value; } /** * Stores a value in the specified Meta information property. * The first argument should be the key to store the value under, * the second argument should be the value. It is common to use * a path-like notation for meta data in RedBeanPHP like: * 'my.meta.data', however the dots are purely for readability, the * meta data methods do not store nested structures or hierarchies. * * @param string $path path / key to store value under * @param mixed $value value to store in bean (not in database) as meta data * * @return OODBBean */ public function setMeta( $path, $value ) { $this->__info[$path] = $value; return $this; } /** * Copies the meta information of the specified bean * This is a convenience method to enable you to * exchange meta information easily. * * @param OODBBean $bean bean to copy meta data of * * @return OODBBean */ public function copyMetaFrom( OODBBean $bean ) { $this->__info = $bean->__info; return $this; } /** * Sends the call to the registered model. * This method can also be used to override bean behaviour. * In that case you don't want an error or exception to be triggered * if the method does not exist in the model (because it's optional). * Unfortunately we cannot add an extra argument to __call() for this * because the signature is fixed. Another option would be to set * a special flag ( i.e. $this->isOptionalCall ) but that would * cause additional complexity because we have to deal with extra temporary state. * So, instead I allowed the method name to be prefixed with '@', in practice * nobody creates methods like that - however the '@' symbol in PHP is widely known * to suppress error handling, so we can reuse the semantics of this symbol. * If a method name gets passed starting with '@' the overrideDontFail variable * will be set to TRUE and the '@' will be stripped from the function name before * attempting to invoke the method on the model. This way, we have all the * logic in one place. * * @param string $method name of the method * @param array $args argument list * * @return mixed */ public function __call( $method, $args ) { $overrideDontFail = FALSE; if ( strpos( $method, '@' ) === 0 ) { $method = substr( $method, 1 ); $overrideDontFail = TRUE; } if ( !isset( $this->__info['model'] ) ) { $model = $this->beanHelper->getModelForBean( $this ); if ( !$model ) { return NULL; } $this->__info['model'] = $model; } if ( !method_exists( $this->__info['model'], $method ) ) { if ( self::$errorHandlingFUSE === FALSE || $overrideDontFail ) { return NULL; } if ( in_array( $method, array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ), TRUE ) ) { return NULL; } $message = "FUSE: method does not exist in model: $method"; if ( self::$errorHandlingFUSE === self::C_ERR_LOG ) { error_log( $message ); return NULL; } elseif ( self::$errorHandlingFUSE === self::C_ERR_NOTICE ) { trigger_error( $message, E_USER_NOTICE ); return NULL; } elseif ( self::$errorHandlingFUSE === self::C_ERR_WARN ) { trigger_error( $message, E_USER_WARNING ); return NULL; } elseif ( self::$errorHandlingFUSE === self::C_ERR_EXCEPTION ) { throw new \Exception( $message ); } elseif ( self::$errorHandlingFUSE === self::C_ERR_FUNC ) { $func = self::$errorHandler; return $func(array( 'message' => $message, 'method' => $method, 'args' => $args, 'bean' => $this )); } trigger_error( $message, E_USER_ERROR ); return NULL; } return call_user_func_array( array( $this->__info['model'], $method ), $args ); } /** * Implementation of __toString Method * Routes call to Model. If the model implements a __toString() method this * method will be called and the result will be returned. In case of an * echo-statement this result will be printed. If the model does not * implement a __toString method, this method will return a JSON * representation of the current bean. * * @return string */ public function __toString() { $string = $this->__call( '@__toString', array() ); if ( $string === NULL ) { $list = array(); foreach($this->properties as $property => $value) { if (is_scalar($value)) { if ( self::$enforceUTF8encoding ) { $list[$property] = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); } else { $list[$property] = $value; } } } $data = json_encode( $list ); return $data; } else { return $string; } } /** * Implementation of Array Access Interface, you can access bean objects * like an array. * Call gets routed to __set. * * @param mixed $offset offset string * @param mixed $value value * * @return void */ public function offsetSet( $offset, $value ) { $this->__set( $offset, $value ); } /** * Implementation of Array Access Interface, you can access bean objects * like an array. * * Array functions do not reveal x-own-lists and list-alias because * you dont want duplicate entries in foreach-loops. * Also offers a slight performance improvement for array access. * * @param mixed $offset property * * @return boolean */ public function offsetExists( $offset ) { return $this->__isset( $offset ); } /** * Implementation of Array Access Interface, you can access bean objects * like an array. * Unsets a value from the array/bean. * * Array functions do not reveal x-own-lists and list-alias because * you dont want duplicate entries in foreach-loops. * Also offers a slight performance improvement for array access. * * @param mixed $offset property * * @return void */ public function offsetUnset( $offset ) { $this->__unset( $offset ); } /** * Implementation of Array Access Interface, you can access bean objects * like an array. * Returns value of a property. * * Array functions do not reveal x-own-lists and list-alias because * you dont want duplicate entries in foreach-loops. * Also offers a slight performance improvement for array access. * * @param mixed $offset property * * @return mixed */ public function &offsetGet( $offset ) { return $this->__get( $offset ); } /** * Chainable method to cast a certain ID to a bean; for instance: * $person = $club->fetchAs('person')->member; * This will load a bean of type person using member_id as ID. * * @param string $type preferred fetch type * * @return OODBBean */ public function fetchAs( $type ) { $this->fetchType = $type; return $this; } /** * For polymorphic bean relations. * Same as fetchAs but uses a column instead of a direct value. * * @param string $field field name to use for mapping * * @return OODBBean */ public function poly( $field ) { return $this->fetchAs( $this->$field ); } /** * Traverses a bean property with the specified function. * Recursively iterates through the property invoking the * function for each bean along the way passing the bean to it. * * Can be used together with with, withCondition, alias and fetchAs. * * @param string $property property * @param callable $function function * @param integer $maxDepth maximum depth for traversal * * @return OODBBean * @throws RedException */ public function traverse( $property, $function, $maxDepth = NULL ) { $this->via = NULL; if ( strpos( $property, 'shared' ) !== FALSE ) { throw new RedException( 'Traverse only works with (x)own-lists.' ); } if ( !is_null( $maxDepth ) ) { if ( !$maxDepth-- ) return $this; } $oldFetchType = $this->fetchType; $oldAliasName = $this->aliasName; $oldWith = $this->withSql; $oldBindings = $this->withParams; $beans = $this->$property; if ( $beans === NULL ) return $this; if ( !is_array( $beans ) ) $beans = array( $beans ); foreach( $beans as $bean ) { /** @var OODBBean $bean */ $function( $bean ); $bean->fetchType = $oldFetchType; $bean->aliasName = $oldAliasName; $bean->withSql = $oldWith; $bean->withParams = $oldBindings; $bean->traverse( $property, $function, $maxDepth ); } return $this; } /** * Implementation of Countable interface. Makes it possible to use * count() function on a bean. * * @return integer */ public function count() { return count( $this->properties ); } /** * Checks whether a bean is empty or not. * A bean is empty if it has no other properties than the id field OR * if all the other property are empty(). * * @return boolean */ public function isEmpty() { $empty = TRUE; foreach ( $this->properties as $key => $value ) { if ( $key == 'id' ) { continue; } if ( !empty( $value ) ) { $empty = FALSE; } } return $empty; } /** * Chainable setter. * * @param string $property the property of the bean * @param mixed $value the value you want to set * * @return OODBBean */ public function setAttr( $property, $value ) { $this->$property = $value; return $this; } /** * Comfort method. * Unsets all properties in array. * * @param array $properties properties you want to unset. * * @return OODBBean */ public function unsetAll( $properties ) { foreach ( $properties as $prop ) { if ( isset( $this->properties[$prop] ) ) { unset( $this->properties[$prop] ); } } return $this; } /** * Returns original (old) value of a property. * You can use this method to see what has changed in a * bean. * * @param string $property name of the property you want the old value of * * @return mixed */ public function old( $property ) { $old = $this->getMeta( 'sys.orig', array() ); if ( array_key_exists( $property, $old ) ) { return $old[$property]; } return NULL; } /** * Convenience method. * Returns TRUE if the bean has been changed, or FALSE otherwise. * Same as $bean->getMeta('tainted'); * Note that a bean becomes tainted as soon as you retrieve a list from * the bean. This is because the bean lists are arrays and the bean cannot * determine whether you have made modifications to a list so RedBeanPHP * will mark the whole bean as tainted. * * @return boolean */ public function isTainted() { return $this->getMeta( 'tainted' ); } /** * Returns TRUE if the value of a certain property of the bean has been changed and * FALSE otherwise. * * Note that this method will return TRUE if applied to a loaded list. * Also note that this method keeps track of the bean's history regardless whether * it has been stored or not. Storing a bean does not undo it's history, * to clean the history of a bean use: clearHistory(). * * @param string $property name of the property you want the change-status of * * @return boolean */ public function hasChanged( $property ) { return ( array_key_exists( $property, $this->properties ) ) ? $this->old( $property ) != $this->properties[$property] : FALSE; } /** * Returns TRUE if the specified list exists, has been loaded and has been changed: * beans have been added or deleted. This method will not tell you anything about * the state of the beans in the list. * * @param string $property name of the list to check * * @return boolean */ public function hasListChanged( $property ) { if ( !array_key_exists( $property, $this->properties ) ) return FALSE; $diffAdded = array_diff_assoc( $this->properties[$property], $this->__info['sys.shadow.'.$property] ); if ( count( $diffAdded ) ) return TRUE; $diffMissing = array_diff_assoc( $this->__info['sys.shadow.'.$property], $this->properties[$property] ); if ( count( $diffMissing ) ) return TRUE; return FALSE; } /** * Clears (syncs) the history of the bean. * Resets all shadow values of the bean to their current value. * * @return self */ public function clearHistory() { $this->__info['sys.orig'] = array(); foreach( $this->properties as $key => $value ) { if ( is_scalar($value) ) { $this->__info['sys.orig'][$key] = $value; } else { $this->__info['sys.shadow.'.$key] = $value; } } $this->__info[ 'changelist' ] = array(); return $this; } /** * Creates a N-M relation by linking an intermediate bean. * This method can be used to quickly connect beans using indirect * relations. For instance, given an album and a song you can connect the two * using a track with a number like this: * * Usage: * * * $album->link('track', array('number'=>1))->song = $song; * * * or: * * * $album->link($trackBean)->song = $song; * * * What this method does is adding the link bean to the own-list, in this case * ownTrack. If the first argument is a string and the second is an array or * a JSON string then the linking bean gets dispensed on-the-fly as seen in * example #1. After preparing the linking bean, the bean is returned thus * allowing the chained setter: ->song = $song. * * @param string|OODBBean $typeOrBean type of bean to dispense or the full bean * @param string|array $qualification JSON string or array (optional) * * @return OODBBean */ public function link( $typeOrBean, $qualification = array() ) { if ( is_string( $typeOrBean ) ) { $typeOrBean = AQueryWriter::camelsSnake( $typeOrBean ); $bean = $this->beanHelper->getToolBox()->getRedBean()->dispense( $typeOrBean ); if ( is_string( $qualification ) ) { $data = json_decode( $qualification, TRUE ); } else { $data = $qualification; } foreach ( $data as $key => $value ) { $bean->$key = $value; } } else { $bean = $typeOrBean; } $list = 'own' . ucfirst( $bean->getMeta( 'type' ) ); array_push( $this->$list, $bean ); return $bean; } /** * Returns a bean of the given type with the same ID of as * the current one. This only happens in a one-to-one relation. * This is as far as support for 1-1 goes in RedBeanPHP. This * method will only return a reference to the bean, changing it * and storing the bean will not update the related one-bean. * * @param $type type of bean to load * * @return OODBBean */ public function one( $type ) { return $this->beanHelper->getToolBox()->getRedBean()->load( $type, $this->id ); } /** * Returns the same bean freshly loaded from the database. * * @return OODBBean */ public function fresh() { return $this->beanHelper->getToolbox()->getRedBean()->load( $this->getMeta( 'type' ), $this->properties['id'] ); } /** * Registers a association renaming globally. * * @param string $via type you wish to use for shared lists * * @return OODBBean */ public function via( $via ) { $this->via = AQueryWriter::camelsSnake( $via ); return $this; } /** * Counts all own beans of type $type. * Also works with alias(), with() and withCondition(). * * @param string $type the type of bean you want to count * * @return integer */ public function countOwn( $type ) { $type = $this->beau( $type ); if ( $this->aliasName ) { $myFieldLink = $this->aliasName . '_id'; $this->aliasName = NULL; } else { $myFieldLink = $this->__info['type'] . '_id'; } $count = 0; if ( $this->getID() ) { $firstKey = NULL; if ( count( $this->withParams ) > 0 ) { reset( $this->withParams ); $firstKey = key( $this->withParams ); } $joinSql = $this->parseJoin( $type ); if ( !is_numeric( $firstKey ) || $firstKey === NULL ) { $bindings = $this->withParams; $bindings[':slot0'] = $this->getID(); $count = $this->beanHelper->getToolbox()->getWriter()->queryRecordCount( $type, array(), " {$joinSql} $myFieldLink = :slot0 " . $this->withSql, $bindings ); } else { $bindings = array_merge( array( $this->getID() ), $this->withParams ); $count = $this->beanHelper->getToolbox()->getWriter()->queryRecordCount( $type, array(), " {$joinSql} $myFieldLink = ? " . $this->withSql, $bindings ); } } $this->clearModifiers(); return (int) $count; } /** * Counts all shared beans of type $type. * Also works with via(), with() and withCondition(). * * @param string $type type of bean you wish to count * * @return integer */ public function countShared( $type ) { $toolbox = $this->beanHelper->getToolbox(); $redbean = $toolbox->getRedBean(); $writer = $toolbox->getWriter(); if ( $this->via ) { $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) ); if ( $oldName !== $this->via ) { //set the new renaming rule $writer->renameAssocTable( $oldName, $this->via ); $this->via = NULL; } } $type = $this->beau( $type ); $count = 0; if ( $this->getID() ) { $count = $redbean->getAssociationManager()->relatedCount( $this, $type, $this->withSql, $this->withParams ); } $this->clearModifiers(); return (integer) $count; } /** * Iterates through the specified own-list and * fetches all properties (with their type) and * returns the references. * Use this method to quickly load indirectly related * beans in an own-list. Whenever you cannot use a * shared-list this method offers the same convenience * by aggregating the parent beans of all children in * the specified own-list. * * Example: * * * $quest->aggr( 'xownQuestTarget', 'target', 'quest' ); * * * Loads (in batch) and returns references to all * quest beans residing in the $questTarget->target properties * of each element in the xownQuestTargetList. * * @param string $list the list you wish to process * @param string $property the property to load * @param string $type the type of bean residing in this property (optional) * * @return array */ public function &aggr( $list, $property, $type = NULL ) { $this->via = NULL; $ids = $beanIndex = $references = array(); if ( strlen( $list ) < 4 ) throw new RedException('Invalid own-list.'); if ( strpos( $list, 'own') !== 0 ) throw new RedException('Only own-lists can be aggregated.'); if ( !ctype_upper( substr( $list, 3, 1 ) ) ) throw new RedException('Invalid own-list.'); if ( is_null( $type ) ) $type = $property; foreach( $this->$list as $bean ) { $field = $property . '_id'; if ( isset( $bean->$field) ) { $ids[] = $bean->$field; $beanIndex[$bean->$field] = $bean; } } $beans = $this->beanHelper->getToolBox()->getRedBean()->batch( $type, $ids ); //now preload the beans as well foreach( $beans as $bean ) { $beanIndex[$bean->id]->setProperty( $property, $bean ); } foreach( $beanIndex as $indexedBean ) { $references[] = $indexedBean->$property; } return $references; } /** * Tests whether the database identities of two beans are equal. * * @param OODBBean $bean other bean * * @return boolean */ public function equals(OODBBean $bean) { return (bool) ( ( (string) $this->properties['id'] === (string) $bean->properties['id'] ) && ( (string) $this->__info['type'] === (string) $bean->__info['type'] ) ); } /** * Magic method jsonSerialize, implementation for the \JsonSerializable interface, * this method gets called by json_encode and facilitates a better JSON representation * of the bean. Exports the bean on JSON serialization, for the JSON fans. * * @see http://php.net/manual/en/class.jsonserializable.php * * @return array */ public function jsonSerialize() { return $this->export(); } } } namespace RedBeanPHP { use RedBeanPHP\Observer as Observer; /** * Observable * Base class for Observables * * @file RedBeanPHP/Observable.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 Observable { //bracket must be here - otherwise coverage software does not understand. /** * @var array */ private $observers = array(); /** * Implementation of the Observer Pattern. * Adds an event listener to the observable object. * First argument should be the name of the event you wish to listen for. * Second argument should be the object that wants to be notified in case * the event occurs. * * @param string $eventname event identifier * @param Observer $observer observer instance * * @return void */ public function addEventListener( $eventname, Observer $observer ) { if ( !isset( $this->observers[$eventname] ) ) { $this->observers[$eventname] = array(); } foreach ( $this->observers[$eventname] as $o ) { if ( $o == $observer ) { return; } } $this->observers[$eventname][] = $observer; } /** * Notifies listeners. * Sends the signal $eventname, the event identifier and a message object * to all observers that have been registered to receive notification for * this event. Part of the observer pattern implementation in RedBeanPHP. * * @param string $eventname event you want signal * @param mixed $info message object to send along * * @return void */ public function signal( $eventname, $info ) { if ( !isset( $this->observers[$eventname] ) ) { $this->observers[$eventname] = array(); } foreach ( $this->observers[$eventname] as $observer ) { $observer->onEvent( $eventname, $info ); } } } } namespace RedBeanPHP { /** * Observer. * * Interface for Observer object. Implementation of the * observer pattern. * * @file RedBeanPHP/Observer.php * @author Gabor de Mooij and the RedBeanPHP community * @license BSD/GPLv2 * @desc Part of the observer pattern in RedBean * * @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. */ interface Observer { /** * An observer object needs to be capable of receiving * notifications. Therefore the observer needs to implement the * onEvent method with two parameters: the event identifier specifying the * current event and a message object (in RedBeanPHP this can also be a bean). * * @param string $eventname event identifier * @param mixed $bean a message sent along with the notification * * @return void */ public function onEvent( $eventname, $bean ); } } namespace RedBeanPHP { /** * Adapter Interface. * Describes the API for a RedBeanPHP Database Adapter. * This interface defines the API contract for * a RedBeanPHP Database Adapter. * * @file RedBeanPHP/Adapter.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @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. */ interface Adapter { /** * Should returns a string containing the most recent SQL query * that has been processed by the adapter. * * @return string */ public function getSQL(); /** * Executes an SQL Statement using an array of values to bind * If $noevent is TRUE then this function will not signal its * observers to notify about the SQL execution; this to prevent * infinite recursion when using observers. * * @param string $sql string containing SQL code for database * @param array $bindings array of values to bind to parameters in query string * @param boolean $noevent no event firing * * @return void */ public function exec( $sql, $bindings = array(), $noevent = FALSE ); /** * Executes an SQL Query and returns a resultset. * This method returns a multi dimensional resultset similar to getAll * The values array can be used to bind values to the place holders in the * SQL query. * * @param string $sql string containing SQL code for database * @param array $bindings array of values to bind to parameters in query string * * @return array */ public function get( $sql, $bindings = array() ); /** * Executes an SQL Query and returns a resultset. * This method returns a single row (one array) resultset. * The values array can be used to bind values to the place holders in the * SQL query. * * @param string $sql string containing SQL code for database * @param array $bindings array of values to bind to parameters in query string * * @return array */ public function getRow( $sql, $bindings = array() ); /** * Executes an SQL Query and returns a resultset. * This method returns a single column (one array) resultset. * The values array can be used to bind values to the place holders in the * SQL query. * * @param string $sql string containing SQL code for database * @param array $bindings array of values to bind to parameters in query string * * @return array */ public function getCol( $sql, $bindings = array() ); /** * Executes an SQL Query and returns a resultset. * This method returns a single cell, a scalar value as the resultset. * The values array can be used to bind values to the place holders in the * SQL query. * * @param string $sql string containing SQL code for database * @param array $bindings array of values to bind to parameters in query string * * @return string */ public function getCell( $sql, $bindings = array() ); /** * Executes the SQL query specified in $sql and takes * the first two columns of the resultset. This function transforms the * resultset into an associative array. Values from the the first column will * serve as keys while the values of the second column will be used as values. * The values array can be used to bind values to the place holders in the * SQL query. * * @param string $sql string containing SQL code for database * @param array $bindings array of values to bind to parameters in query string * * @return array */ public function getAssoc( $sql, $bindings = array() ); /** * Executes the SQL query specified in $sql and indexes * the row by the first column. * * @param string $sql Sstring containing SQL code for databaseQL * @param array $bindings values to bind * * @return array */ public function getAssocRow( $sql, $bindings = array() ); /** * Returns the latest insert ID. * * @return integer */ public function getInsertID(); /** * Returns the number of rows that have been * affected by the last update statement. * * @return integer */ public function getAffectedRows(); /** * Returns a database agnostic Cursor object. * * @param string $sql string containing SQL code for database * @param array $bindings array of values to bind to parameters in query string * * @return Cursor */ public function getCursor( $sql, $bindings = array() ); /** * Returns the original database resource. This is useful if you want to * perform operations on the driver directly instead of working with the * adapter. RedBean will only access the adapter and never to talk * directly to the driver though. * * @return mixed */ public function getDatabase(); /** * This method is part of the RedBean Transaction Management * mechanisms. * Starts a transaction. * * @return void */ public function startTransaction(); /** * This method is part of the RedBean Transaction Management * mechanisms. * Commits the transaction. * * @return void */ public function commit(); /** * This method is part of the RedBean Transaction Management * mechanisms. * Rolls back the transaction. * * @return void */ public function rollback(); /** * Closes database connection. * * @return void */ public function close(); /** * Sets a driver specific option. * Using this method you can access driver-specific functions. * If the selected option exists the value will be passed and * this method will return boolean TRUE, otherwise it will return * boolean FALSE. * * @param string $optionKey option key * @param string $optionValue option value * * @return boolean */ public function setOption( $optionKey, $optionValue ); } } namespace RedBeanPHP\Adapter { use RedBeanPHP\Observable as Observable; use RedBeanPHP\Adapter as Adapter; use RedBeanPHP\Driver as Driver; /** * DBAdapter (Database Adapter) * * An adapter class to connect various database systems to RedBean * Database Adapter Class. The task of the database adapter class is to * communicate with the database driver. You can use all sorts of database * drivers with RedBeanPHP. The default database drivers that ships with * the RedBeanPHP library is the RPDO driver ( which uses the PHP Data Objects * Architecture aka PDO ). * * @file RedBeanPHP/Adapter/DBAdapter.php * @author Gabor de Mooij and the RedBeanPHP Community. * @license BSD/GPLv2 * * @copyright * (c) copyright 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. */ class DBAdapter extends Observable implements Adapter { /** * @var Driver */ private $db = NULL; /** * @var string */ private $sql = ''; /** * Constructor. * * Creates an instance of the RedBean Adapter Class. * This class provides an interface for RedBean to work * with ADO compatible DB instances. * * @param Driver $database ADO Compatible DB Instance */ public function __construct( $database ) { $this->db = $database; } /** * Returns a string containing the most recent SQL query * processed by the database adapter, thus conforming to the * interface: * * @see Adapter::getSQL * * Methods like get(), getRow() and exec() cause this SQL cache * to get filled. If no SQL query has been processed yet this function * will return an empty string. * * @return string */ public function getSQL() { return $this->sql; } /** * @see Adapter::exec */ public function exec( $sql, $bindings = array(), $noevent = FALSE ) { if ( !$noevent ) { $this->sql = $sql; $this->signal( 'sql_exec', $this ); } return $this->db->Execute( $sql, $bindings ); } /** * @see Adapter::get */ public function get( $sql, $bindings = array() ) { $this->sql = $sql; $this->signal( 'sql_exec', $this ); return $this->db->GetAll( $sql, $bindings ); } /** * @see Adapter::getRow */ public function getRow( $sql, $bindings = array() ) { $this->sql = $sql; $this->signal( 'sql_exec', $this ); return $this->db->GetRow( $sql, $bindings ); } /** * @see Adapter::getCol */ public function getCol( $sql, $bindings = array() ) { $this->sql = $sql; $this->signal( 'sql_exec', $this ); return $this->db->GetCol( $sql, $bindings ); } /** * @see Adapter::getAssoc */ public function getAssoc( $sql, $bindings = array() ) { $this->sql = $sql; $this->signal( 'sql_exec', $this ); $rows = $this->db->GetAll( $sql, $bindings ); $assoc = array(); if ( !$rows ) { return $assoc; } foreach ( $rows as $row ) { if ( empty( $row ) ) continue; if ( count( $row ) > 2 ) { $key = array_shift( $row ); $value = $row; } elseif ( count( $row ) > 1 ) { $key = array_shift( $row ); $value = array_shift( $row ); } else { $key = array_shift( $row ); $value = $key; } $assoc[$key] = $value; } return $assoc; } /** * @see Adapter::getAssocRow */ public function getAssocRow($sql, $bindings = array()) { $this->sql = $sql; $this->signal( 'sql_exec', $this ); return $this->db->GetAssocRow( $sql, $bindings ); } /** * @see Adapter::getCell */ public function getCell( $sql, $bindings = array(), $noSignal = NULL ) { $this->sql = $sql; if ( !$noSignal ) $this->signal( 'sql_exec', $this ); return $this->db->GetOne( $sql, $bindings ); } /** * @see Adapter::getCursor */ public function getCursor( $sql, $bindings = array() ) { return $this->db->GetCursor( $sql, $bindings ); } /** * @see Adapter::getInsertID */ public function getInsertID() { return $this->db->getInsertID(); } /** * @see Adapter::getAffectedRows */ public function getAffectedRows() { return $this->db->Affected_Rows(); } /** * @see Adapter::getDatabase */ public function getDatabase() { return $this->db; } /** * @see Adapter::startTransaction */ public function startTransaction() { $this->db->StartTrans(); } /** * @see Adapter::commit */ public function commit() { $this->db->CommitTrans(); } /** * @see Adapter::rollback */ public function rollback() { $this->db->FailTrans(); } /** * @see Adapter::close. */ public function close() { $this->db->close(); } /** * @see Adapter::setOption */ public function setOption( $optionKey, $optionValue ) { if ( method_exists( $this->db, $optionKey ) ) { call_user_func( array( $this->db, $optionKey ), $optionValue ); return TRUE; } return FALSE; } } } namespace RedBeanPHP { /** * Database Cursor Interface. * A cursor is used by Query Writers to fetch Query Result rows * one row at a time. This is useful if you expect the result set to * be quite large. This interface dscribes the API of a database * cursor. There can be multiple implementations of the Cursor, * by default RedBeanPHP offers the PDOCursor for drivers shipping * with RedBeanPHP and the NULLCursor. * * @file RedBeanPHP/Cursor.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. */ interface Cursor { /** * Should retrieve the next row of the result set. * This method is used to iterate over the result set. * * @return array */ public function getNextItem(); /** * Closes the database cursor. * Some databases require a cursor to be closed before executing * another statement/opening a new cursor. * * @return void */ public function close(); } } namespace RedBeanPHP\Cursor { use RedBeanPHP\Cursor as Cursor; /** * PDO Database Cursor * Implementation of PDO Database Cursor. * Used by the BeanCollection to fetch one bean at a time. * The PDO Cursor is used by Query Writers to support retrieval * of large bean collections. For instance, this class is used to * implement the findCollection()/BeanCollection functionality. * * @file RedBeanPHP/Cursor/PDOCursor.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @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. */ class PDOCursor implements Cursor { /** * @var PDOStatement */ protected $res; /** * @var string */ protected $fetchStyle; /** * Constructor, creates a new instance of a PDO Database Cursor. * * @param PDOStatement $res the PDO statement * @param string $fetchStyle fetch style constant to use * * @return void */ public function __construct( \PDOStatement $res, $fetchStyle ) { $this->res = $res; $this->fetchStyle = $fetchStyle; } /** * @see Cursor::getNextItem */ public function getNextItem() { return $this->res->fetch(); } /** * @see Cursor::close */ public function close() { $this->res->closeCursor(); } } } namespace RedBeanPHP\Cursor { use RedBeanPHP\Cursor as Cursor; /** * NULL Database Cursor * Implementation of the NULL Cursor. * Used for an empty BeanCollection. This Cursor * can be used for instance if a query fails but the interface * demands a cursor to be returned. * * @file RedBeanPHP/Cursor/NULLCursor.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @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. */ class NullCursor implements Cursor { /** * @see Cursor::getNextItem */ public function getNextItem() { return NULL; } /** * @see Cursor::close */ public function close() { return NULL; } } } namespace RedBeanPHP { use RedBeanPHP\Cursor as Cursor; use RedBeanPHP\Repository as Repository; /** * BeanCollection. * * 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. * * Use the BeanCollection for large datasets where skip/limit is not an * option. Keep in mind that ID-marking (querying a start ID) is a decent * alternative though. * * @file RedBeanPHP/BeanCollection.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. */ class BeanCollection { /** * @var Cursor */ protected $cursor = NULL; /** * @var Repository */ protected $repository = NULL; /** * @var string */ protected $type = NULL; /** * Constructor, creates a new instance of the BeanCollection. * * @param string $type type of beans in this collection * @param Repository $repository repository to use to generate bean objects * @param Cursor $cursor cursor object to use * * @return void */ public function __construct( $type, Repository $repository, Cursor $cursor ) { $this->type = $type; $this->cursor = $cursor; $this->repository = $repository; } /** * Returns the next bean in the collection. * If called the first time, this will return the first bean in the collection. * If there are no more beans left in the collection, this method * will return NULL. * * @return OODBBean|NULL */ public function next() { $row = $this->cursor->getNextItem(); if ( $row ) { $beans = $this->repository->convertToBeans( $this->type, array( $row ) ); $bean = array_shift( $beans ); return $bean; } return NULL; } /** * Closes the underlying cursor (needed for some databases). * * @return void */ public function close() { $this->cursor->close(); } } } namespace RedBeanPHP { /** * QueryWriter * Interface for QueryWriters. * Describes the API for a QueryWriter. * * Terminology: * * - beautified property (a camelCased property, has to be converted first) * - beautified type (a camelCased type, has to be converted first) * - type (a bean type, corresponds directly to a table) * - property (a bean property, corresponds directly to a column) * - table (a checked and quoted type, ready for use in a query) * - column (a checked and quoted property, ready for use in query) * - tableNoQ (same as type, but in context of a database operation) * - columnNoQ (same as property, but in context of a database operation) * * @file RedBeanPHP/QueryWriter.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. */ interface QueryWriter { /** * SQL filter constants */ const C_SQLFILTER_READ = 'r'; const C_SQLFILTER_WRITE = 'w'; /** * Query Writer constants. */ const C_SQLSTATE_NO_SUCH_TABLE = 1; const C_SQLSTATE_NO_SUCH_COLUMN = 2; const C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION = 3; const C_SQLSTATE_LOCK_TIMEOUT = 4; /** * Define data type regions * * 00 - 80: normal data types * 80 - 99: special data types, only scan/code if requested * 99 : specified by user, don't change */ const C_DATATYPE_RANGE_SPECIAL = 80; const C_DATATYPE_RANGE_SPECIFIED = 99; /** * Define GLUE types for use with glueSQLCondition methods. * Determines how to prefix a snippet of SQL before appending it * to other SQL (or integrating it, mixing it otherwise). * * WHERE - glue as WHERE condition * AND - glue as AND condition */ const C_GLUE_WHERE = 1; const C_GLUE_AND = 2; /** * Writes an SQL Snippet for a JOIN, returns the * SQL snippet string. * * @note A default implementation is available in AQueryWriter * unless a database uses very different SQL this should suffice. * * @param string $type source type * @param string $targetType target type (type to join) * @param string $joinType type of join (possible: 'LEFT', 'RIGHT' or 'INNER'). * * @return string $joinSQLSnippet */ public function writeJoin( $type, $targetType, $joinType ); /** * Glues an SQL snippet to the beginning of a WHERE clause. * This ensures users don't have to add WHERE to their query snippets. * * The snippet gets prefixed with WHERE or AND * if it starts with a condition. * * If the snippet does NOT start with a condition (or this function thinks so) * the snippet is returned as-is. * * The GLUE type determines the prefix: * * * NONE prefixes with WHERE * * WHERE prefixes with WHERE and replaces AND if snippets starts with AND * * AND prefixes with AND * * This method will never replace WHERE with AND since a snippet should never * begin with WHERE in the first place. OR is not supported. * * Only a limited set of clauses will be recognized as non-conditions. * For instance beginning a snippet with complex statements like JOIN or UNION * will not work. This is too complex for use in a snippet. * * @note A default implementation is available in AQueryWriter * unless a database uses very different SQL this should suffice. * * @param string $sql SQL Snippet * @param integer $glue the GLUE type - how to glue (C_GLUE_WHERE or C_GLUE_AND) * * @return string */ public function glueSQLCondition( $sql, $glue = NULL ); /** * Determines if there is a LIMIT 1 clause in the SQL. * If not, it will add a LIMIT 1. (used for findOne). * * @note A default implementation is available in AQueryWriter * unless a database uses very different SQL this should suffice. * * @param string $sql query to scan and adjust * * @return string */ public function glueLimitOne( $sql ); /** * Returns the tables that are in the database. * * @return array */ public function getTables(); /** * This method will create a table for the bean. * This methods accepts a type and infers the corresponding table name. * * @param string $type type of bean you want to create a table for * * @return void */ public function createTable( $type ); /** * Returns an array containing all the columns of the specified type. * The format of the return array looks like this: * $field => $type where $field is the name of the column and $type * is a database specific description of the datatype. * * This methods accepts a type and infers the corresponding table name. * * @param string $type type of bean you want to obtain a column list of * * @return array */ public function getColumns( $type ); /** * Returns the Column Type Code (integer) that corresponds * to the given value type. This method is used to determine the minimum * column type required to represent the given value. There are two modes of * operation: with or without special types. Scanning without special types * requires the second parameter to be set to FALSE. This is useful when the * column has already been created and prevents it from being modified to * an incompatible type leading to data loss. Special types will be taken * into account when a column does not exist yet (parameter is then set to TRUE). * * Special column types are determines by the AQueryWriter constant * C_DATA_TYPE_ONLY_IF_NOT_EXISTS (usually 80). Another 'very special' type is type * C_DATA_TYPE_MANUAL (usually 99) which represents a user specified type. Although * no special treatment has been associated with the latter for now. * * @param string $value value * @param boolean $alsoScanSpecialForTypes take special types into account * * @return integer */ public function scanType( $value, $alsoScanSpecialForTypes = FALSE ); /** * This method will add a column to a table. * This methods accepts a type and infers the corresponding table name. * * @param string $type name of the table * @param string $column name of the column * @param integer $field data type for field * * @return void */ public function addColumn( $type, $column, $field ); /** * Returns the Type Code for a Column Description. * Given an SQL column description this method will return the corresponding * code for the writer. If the include specials flag is set it will also * return codes for special columns. Otherwise special columns will be identified * as specified columns. * * @param string $typedescription description * @param boolean $includeSpecials whether you want to get codes for special columns as well * * @return integer */ public function code( $typedescription, $includeSpecials = FALSE ); /** * This method will widen the column to the specified data type. * This methods accepts a type and infers the corresponding table name. * * @param string $type type / table that needs to be adjusted * @param string $column column that needs to be altered * @param integer $datatype target data type * * @return void */ public function widenColumn( $type, $column, $datatype ); /** * Selects records from the database. * This methods selects the records from the database that match the specified * type, conditions (optional) and additional SQL snippet (optional). * * @param string $type name of the table you want to query * @param array $conditions criteria ( $column => array( $values ) ) * @param string $addSql additional SQL snippet * @param array $bindings bindings for SQL snippet * * @return array */ public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ); /** * Selects records from the database and returns a cursor. * This methods selects the records from the database that match the specified * type, conditions (optional) and additional SQL snippet (optional). * * @param string $type name of the table you want to query * @param array $conditions criteria ( $column => array( $values ) ) * @param string $addSQL additional SQL snippet * @param array $bindings bindings for SQL snippet * * @return Cursor */ public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() ); /** * Returns records through an intermediate type. This method is used to obtain records using a link table and * allows the SQL snippets to reference columns in the link table for additional filtering or ordering. * * @param string $sourceType source type, the reference type you want to use to fetch related items on the other side * @param string $destType destination type, the target type you want to get beans of * @param mixed $linkID ID to use for the link table * @param string $addSql Additional SQL snippet * @param array $bindings Bindings for SQL snippet * * @return array */ public function queryRecordRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() ); /** * Returns the row that links $sourceType $sourcID to $destType $destID in an N-M relation. * * @param string $sourceType source type, the first part of the link you're looking for * @param string $destType destination type, the second part of the link you're looking for * @param string $sourceID ID for the source * @param string $destID ID for the destination * * @return array|null */ public function queryRecordLink( $sourceType, $destType, $sourceID, $destID ); /** * Counts the number of records in the database that match the * conditions and additional SQL. * * @param string $type name of the table you want to query * @param array $conditions criteria ( $column => array( $values ) ) * @param string $addSQL additional SQL snippet * @param array $bindings bindings for SQL snippet * * @return integer */ public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() ); /** * Returns the number of records linked through $linkType and satisfying the SQL in $addSQL/$bindings. * * @param string $sourceType source type * @param string $targetType the thing you want to count * @param mixed $linkID the of the source type * @param string $addSQL additional SQL snippet * @param array $bindings bindings for SQL snippet * * @return integer */ public function queryRecordCountRelated( $sourceType, $targetType, $linkID, $addSQL = '', $bindings = array() ); /** * Returns all rows of specified type that have been tagged with one of the * strings in the specified tag list array. * * Note that the additional SQL snippet can only be used for pagination, * the SQL snippet will be appended to the end of the query. * * @param string $type the bean type you want to query * @param array $tagList an array of strings, each string containing a tag title * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list * @param string $addSql addition SQL snippet, for pagination * @param array $bindings parameter bindings for additional SQL snippet * * @return array */ public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ); /** * This method should update (or insert a record), it takes * a table name, a list of update values ( $field => $value ) and an * primary key ID (optional). If no primary key ID is provided, an * INSERT will take place. * Returns the new ID. * This methods accepts a type and infers the corresponding table name. * * @param string $type name of the table to update * @param array $updatevalues list of update values * @param integer $id optional primary key ID value * * @return integer */ public function updateRecord( $type, $updatevalues, $id = NULL ); /** * Deletes records from the database. * @note $addSql is always prefixed with ' WHERE ' or ' AND .' * * @param string $type name of the table you want to query * @param array $conditions criteria ( $column => array( $values ) ) * @param string $addSql additional SQL * @param array $bindings bindings * * @return void */ public function deleteRecord( $type, $conditions = array(), $addSql = '', $bindings = array() ); /** * Deletes all links between $sourceType and $destType in an N-M relation. * * @param string $sourceType source type * @param string $destType destination type * @param string $sourceID source ID * * @return void */ public function deleteRelations( $sourceType, $destType, $sourceID ); /** * @see QueryWriter::addUniqueConstaint */ public function addUniqueIndex( $type, $columns ); /** * This method will add a UNIQUE constraint index to a table on columns $columns. * This methods accepts a type and infers the corresponding table name. * * @param string $type target bean type * @param array $columnsPartOfIndex columns to include in index * * @return void */ public function addUniqueConstraint( $type, $columns ); /** * This method will check whether the SQL state is in the list of specified states * and returns TRUE if it does appear in this list or FALSE if it * does not. The purpose of this method is to translate the database specific state to * a one of the constants defined in this class and then check whether it is in the list * of standard states provided. * * @param string $state SQL state to consider * @param array $list list of standardized SQL state constants to check against * @param array $extraDriverDetails Some databases communicate state information in a driver-specific format * rather than through the main sqlState code. For those databases, this extra * information can be used to determine the standardized state * * @return boolean */ public function sqlStateIn( $state, $list, $extraDriverDetails = array() ); /** * This method will remove all beans of a certain type. * This methods accepts a type and infers the corresponding table name. * * @param string $type bean type * * @return void */ public function wipe( $type ); /** * This method will add a foreign key from type and field to * target type and target field. * The foreign key is created without an action. On delete/update * no action will be triggered. The FK is only used to allow database * tools to generate pretty diagrams and to make it easy to add actions * later on. * This methods accepts a type and infers the corresponding table name. * * * @param string $type type that will have a foreign key field * @param string $targetType points to this type * @param string $property field that contains the foreign key value * @param string $targetProperty field where the fk points to * @param string $isDep whether target is dependent and should cascade on update/delete * * @return void */ public function addFK( $type, $targetType, $property, $targetProperty, $isDep = false ); /** * This method will add an index to a type and field with name * $name. * This methods accepts a type and infers the corresponding table name. * * @param string $type type to add index to * @param string $name name of the new index * @param string $property field to index * * @return void */ public function addIndex( $type, $name, $property ); /** * Checks and filters a database structure element like a table of column * for safe use in a query. A database structure has to conform to the * RedBeanPHP DB security policy which basically means only alphanumeric * symbols are allowed. This security policy is more strict than conventional * SQL policies and does therefore not require database specific escaping rules. * * @param string $databaseStructure name of the column/table to check * @param boolean $noQuotes TRUE to NOT put backticks or quotes around the string * * @return string */ public function esc( $databaseStructure, $dontQuote = FALSE ); /** * Removes all tables and views from the database. * * @return void */ public function wipeAll(); /** * Renames an association. For instance if you would like to refer to * album_song as: track you can specify this by calling this method like: * * * renameAssociation('album_song','track') * * * This allows: * * * $album->sharedSong * * * to add/retrieve beans from track instead of album_song. * Also works for exportAll(). * * This method also accepts a single associative array as * its first argument. * * @param string|array $fromType original type name, or array * @param string $toType new type name (only if 1st argument is string) * * @return void */ public function renameAssocTable( $fromType, $toType = NULL ); /** * Returns the format for link tables. * Given an array containing two type names this method returns the * name of the link table to be used to store and retrieve * association records. For instance, given two types: person and * project, the corresponding link table might be: 'person_project'. * * @param array $types two types array($type1, $type2) * * @return string */ public function getAssocTable( $types ); /** * Given a bean type and a property, this method * tries to infer the fetch type using the foreign key * definitions in the database. * For instance: project, student -> person. * If no fetchType can be inferred, this method will return NULL. * * @note QueryWriters do not have to implement this method, * it's optional. A default version is available in AQueryWriter. * * @param $type the source type to fetch a target type for * @param $property the property to fetch the type of * * @return string|NULL */ public function inferFetchType( $type, $property ); } } namespace RedBeanPHP\QueryWriter { use RedBeanPHP\Adapter\DBAdapter as DBAdapter; use RedBeanPHP\RedException as RedException; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\RedException\SQL as SQLException; /** * RedBeanPHP Abstract Query Writer. * Represents an abstract Database to RedBean * To write a driver for a different database for RedBean * Contains a number of functions all implementors can * inherit or override. * * @file RedBeanPHP/QueryWriter/AQueryWriter.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @copyright * (c) copyright 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 AQueryWriter { /** * Constant: Select Snippet 'FOR UPDATE' */ const C_SELECT_SNIPPET_FOR_UPDATE = 'FOR UPDATE'; const C_DATA_TYPE_ONLY_IF_NOT_EXISTS = 80; const C_DATA_TYPE_MANUAL = 99; /** * @var array */ private static $sqlFilters = array(); /** * @var boolean */ private static $flagSQLFilterSafeMode = false; /** * @var boolean */ private static $flagNarrowFieldMode = true; /** * @var boolean */ protected static $flagUseJSONColumns = FALSE; /** * @var array */ public static $renames = array(); /** * @var DBAdapter */ protected $adapter; /** * @var string */ protected $defaultValue = 'NULL'; /** * @var string */ protected $quoteCharacter = ''; /** * @var boolean */ protected $flagUseCache = TRUE; /** * @var array */ protected $cache = array(); /** * @var integer */ protected $maxCacheSizePerType = 20; /** * @var string */ protected $sqlSelectSnippet = ''; /** * @var array */ public $typeno_sqltype = array(); /** * Toggles support for automatic generation of JSON columns. * Using JSON columns means that strings containing JSON will * cause the column to be created (not modified) as a JSON column. * However it might also trigger exceptions if this means the DB attempts to * convert a non-json column to a JSON column. Returns the previous * value of the flag. * * @param boolean $flag TRUE or FALSE * * @return boolean */ public static function useJSONColumns( $flag ) { $old = self::$flagUseJSONColumns; self::$flagUseJSONColumns = $flag; return $old; } /** * Checks whether a number can be treated like an int. * * @param string $value string representation of a certain value * * @return boolean */ public static function canBeTreatedAsInt( $value ) { return (bool) ( strval( $value ) === strval( intval( $value ) ) ); } /** * @see QueryWriter::getAssocTableFormat */ public static function getAssocTableFormat( $types ) { sort( $types ); $assoc = implode( '_', $types ); return ( isset( self::$renames[$assoc] ) ) ? self::$renames[$assoc] : $assoc; } /** * @see QueryWriter::renameAssociation */ public static function renameAssociation( $from, $to = NULL ) { if ( is_array( $from ) ) { foreach ( $from as $key => $value ) self::$renames[$key] = $value; return; } self::$renames[$from] = $to; } /** * Globally available service method for RedBeanPHP. * Converts a camel cased string to a snake cased string. * * @param string $camel camelCased string to converty to snake case * * @return string */ public static function camelsSnake( $camel ) { return strtolower( preg_replace( '/(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])/', '_$1$2', $camel ) ); } /** * Clears renames. * * @return void */ public static function clearRenames() { self::$renames = array(); } /** * Toggles 'Narrow Field Mode'. * In Narrow Field mode the queryRecord method will * narrow its selection field to * * SELECT {table}.* * * instead of * * SELECT * * * This is a better way of querying because it allows * more flexibility (for instance joins). However if you need * the wide selector for backward compatibility; use this method * to turn OFF Narrow Field Mode by passing FALSE. * * @param boolean $narrowField TRUE = Narrow Field FALSE = Wide Field * * @return void */ public static function setNarrowFieldMode( $narrowField ) { self::$flagNarrowFieldMode = (boolean) $narrowField; } /** * Sets SQL filters. * This is a lowlevel method to set the SQL filter array. * The format of this array is: * * * array( * '' => array( * '' => array( * '' => '' * ) * ) * ) * * * Example: * * * array( * QueryWriter::C_SQLFILTER_READ => array( * 'book' => array( * 'title' => ' LOWER(book.title) ' * ) * ) * * * Note that you can use constants instead of magical chars * as keys for the uppermost array. * This is a lowlevel method. For a more friendly method * please take a look at the facade: R::bindFunc(). * * @param array list of filters to set * * @return void */ public static function setSQLFilters( $sqlFilters, $safeMode = false ) { self::$flagSQLFilterSafeMode = (boolean) $safeMode; self::$sqlFilters = $sqlFilters; } /** * Returns current SQL Filters. * This method returns the raw SQL filter array. * This is a lowlevel method. For a more friendly method * please take a look at the facade: R::bindFunc(). * * @return array */ public static function getSQLFilters() { return self::$sqlFilters; } /** * Returns a cache key for the cache values passed. * This method returns a fingerprint string to be used as a key to store * data in the writer cache. * * @param array $keyValues key-value to generate key for * * @return string */ private function getCacheKey( $keyValues ) { return json_encode( $keyValues ); } /** * Returns the values associated with the provided cache tag and key. * * @param string $cacheTag cache tag to use for lookup * @param string $key key to use for lookup * * @return mixed */ private function getCached( $cacheTag, $key ) { $sql = $this->adapter->getSQL(); if ($this->updateCache()) { if ( isset( $this->cache[$cacheTag][$key] ) ) { return $this->cache[$cacheTag][$key]; } } return NULL; } /** * Checks if the previous query had a keep-cache tag. * If so, the cache will persist, otherwise the cache will be flushed. * * Returns TRUE if the cache will remain and FALSE if a flush has * been performed. * * @return boolean */ private function updateCache() { $sql = $this->adapter->getSQL(); if ( strpos( $sql, '-- keep-cache' ) !== strlen( $sql ) - 13 ) { // If SQL has been taken place outside of this method then something else then // a select query might have happened! (or instruct to keep cache) $this->cache = array(); return FALSE; } return TRUE; } /** * Stores data from the writer in the cache under a specific key and cache tag. * A cache tag is used to make sure the cache remains consistent. In most cases the cache tag * will be the bean type, this makes sure queries associated with a certain reference type will * never contain conflicting data. * Why not use the cache tag as a key? Well * we need to make sure the cache contents fits the key (and key is based on the cache values). * Otherwise it would be possible to store two different result sets under the same key (the cache tag). * * In previous versions you could only store one key-entry, I have changed this to * improve caching efficiency (issue #400). * * @param string $cacheTag cache tag (secondary key) * @param string $key key to store values under * @param array $values content to be stored * * @return void */ private function putResultInCache( $cacheTag, $key, $values ) { if ( isset( $this->cache[$cacheTag] ) ) { if ( count( $this->cache[$cacheTag] ) > $this->maxCacheSizePerType ) array_shift( $this->cache[$cacheTag] ); } else { $this->cache[$cacheTag] = array(); } $this->cache[$cacheTag][$key] = $values; } /** * Creates an SQL snippet from a list of conditions of format: * * * array( * key => array( * value1, value2, value3 .... * ) * ) * * * @param array $conditions list of conditions * @param array $bindings parameter bindings for SQL snippet * @param string $addSql additional SQL snippet to append to result * * @return string */ private function makeSQLFromConditions( $conditions, &$bindings, $addSql = '' ) { reset( $bindings ); $firstKey = key( $bindings ); $paramTypeIsNum = ( is_numeric( $firstKey ) ); $counter = 0; $sqlConditions = array(); foreach ( $conditions as $column => $values ) { if ( !count( $values ) ) continue; $sql = $this->esc( $column ); $sql .= ' IN ( '; if ( !is_array( $values ) ) $values = array( $values ); if ( $paramTypeIsNum ) { $sql .= implode( ',', array_fill( 0, count( $values ), '?' ) ) . ' ) '; array_unshift($sqlConditions, $sql); foreach ( $values as $k => $v ) { $values[$k] = strval( $v ); array_unshift( $bindings, $v ); } } else { $slots = array(); foreach( $values as $k => $v ) { $slot = ':slot'.$counter++; $slots[] = $slot; $bindings[$slot] = strval( $v ); } $sql .= implode( ',', $slots ).' ) '; $sqlConditions[] = $sql; } } $sql = ''; if ( is_array( $sqlConditions ) && count( $sqlConditions ) > 0 ) { $sql = implode( ' AND ', $sqlConditions ); $sql = " WHERE ( $sql ) "; if ( $addSql ) $sql .= $addSql; } elseif ( $addSql ) { $sql = $addSql; } return $sql; } /** * Returns the table names and column names for a relational query. * * @param string $sourceType type of the source bean * @param string $destType type of the bean you want to obtain using the relation * @param boolean $noQuote TRUE if you want to omit quotes * * @return array */ private function getRelationalTablesAndColumns( $sourceType, $destType, $noQuote = FALSE ) { $linkTable = $this->esc( $this->getAssocTable( array( $sourceType, $destType ) ), $noQuote ); $sourceCol = $this->esc( $sourceType . '_id', $noQuote ); if ( $sourceType === $destType ) { $destCol = $this->esc( $destType . '2_id', $noQuote ); } else { $destCol = $this->esc( $destType . '_id', $noQuote ); } $sourceTable = $this->esc( $sourceType, $noQuote ); $destTable = $this->esc( $destType, $noQuote ); return array( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ); } /** * Determines whether a string can be considered JSON or not. * This is used by writers that support JSON columns. However * we dont want that code duplicated over all JSON supporting * Query Writers. * * @param string $value value to determine 'JSONness' of. * * @return boolean */ protected function isJSON( $value ) { return ( is_string($value) && is_array(json_decode($value, TRUE)) && (json_last_error() == JSON_ERROR_NONE) ); } /** * Given a type and a property name this method * returns the foreign key map section associated with this pair. * * @param string $type name of the type * @param string $property name of the property * * @return array|NULL */ protected function getForeignKeyForTypeProperty( $type, $property ) { $property = $this->esc( $property, TRUE ); try { $map = $this->getKeyMapForType( $type ); } catch ( SQLException $e ) { return NULL; } foreach( $map as $key ) { if ( $key['from'] === $property ) return $key; } return NULL; } /** * Returns the foreign key map (FKM) for a type. * A foreign key map describes the foreign keys in a table. * A FKM always has the same structure: * * * array( * 'name' => * 'from' => * 'table' => * 'to' => (most of the time 'id') * 'on_update' => * 'on_delete' => * ) * * * @note the keys in the result array are FKDLs, i.e. descriptive unique * keys per source table. Also see: AQueryWriter::makeFKLabel for details. * * @param string $type the bean type you wish to obtain a key map of * * @return array */ protected function getKeyMapForType( $type ) { return array(); } /** * This method makes a key for a foreign key description array. * This key is a readable string unique for every source table. * This uniform key is called the FKDL Foreign Key Description Label. * Note that the source table is not part of the FKDL because * this key is supposed to be 'per source table'. If you wish to * include a source table, prefix the key with 'on_table__'. * * @param string $from the column of the key in the source table * @param string $type the type (table) where the key points to * @param string $to the target column of the foreign key (mostly just 'id') * * @return string */ protected function makeFKLabel($from, $type, $to) { return "from_{$from}_to_table_{$type}_col_{$to}"; } /** * Returns an SQL Filter snippet for reading. * * @param string $type type of bean * * @return string */ protected function getSQLFilterSnippet( $type ) { $existingCols = array(); if (self::$flagSQLFilterSafeMode) { $existingCols = $this->getColumns( $type ); } $sqlFilters = array(); if ( isset( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] ) ) { foreach( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] as $property => $sqlFilter ) { if ( !self::$flagSQLFilterSafeMode || isset( $existingCols[$property] ) ) { $sqlFilters[] = $sqlFilter.' AS '.$property.' '; } } } $sqlFilterStr = ( count($sqlFilters) ) ? ( ','.implode( ',', $sqlFilters ) ) : ''; return $sqlFilterStr; } /** * Generates a list of parameters (slots) for an SQL snippet. * This method calculates the correct number of slots to insert in the * SQL snippet and determines the correct type of slot. If the bindings * array contains named parameters this method will return named ones and * update the keys in the value list accordingly (that's why we use the &). * * If you pass an offset the bindings will be re-added to the value list. * Some databases cant handle duplicate parameter names in queries. * * @param array &$valueList list of values to generate slots for (gets modified if needed) * @param array $otherBindings list of additional bindings * @param integer $offset start counter at... * * @return string */ protected function getParametersForInClause( &$valueList, $otherBindings, $offset = 0 ) { if ( is_array( $otherBindings ) && count( $otherBindings ) > 0 ) { reset( $otherBindings ); $key = key( $otherBindings ); if ( !is_numeric($key) ) { $filler = array(); $newList = (!$offset) ? array() : $valueList; $counter = $offset; foreach( $valueList as $value ) { $slot = ':slot' . ( $counter++ ); $filler[] = $slot; $newList[$slot] = $value; } // Change the keys! $valueList = $newList; return implode( ',', $filler ); } } return implode( ',', array_fill( 0, count( $valueList ), '?' ) ); } /** * Adds a data type to the list of data types. * Use this method to add a new column type definition to the writer. * Used for UUID support. * * @param integer $dataTypeID magic number constant assigned to this data type * @param string $SQLDefinition SQL column definition (i.e. INT(11)) * * @return self */ protected function addDataType( $dataTypeID, $SQLDefinition ) { $this->typeno_sqltype[ $dataTypeID ] = $SQLDefinition; $this->sqltype_typeno[ $SQLDefinition ] = $dataTypeID; return $this; } /** * Returns the sql that should follow an insert statement. * * @param string $table name * * @return string */ protected function getInsertSuffix( $table ) { return ''; } /** * Checks whether a value starts with zeros. In this case * the value should probably be stored using a text datatype instead of a * numerical type in order to preserve the zeros. * * @param string $value value to be checked. * * @return boolean */ protected function startsWithZeros( $value ) { $value = strval( $value ); if ( strlen( $value ) > 1 && strpos( $value, '0' ) === 0 && strpos( $value, '0.' ) !== 0 ) { return TRUE; } else { return FALSE; } } /** * Inserts a record into the database using a series of insert columns * and corresponding insertvalues. Returns the insert id. * * @param string $table table to perform query on * @param array $insertcolumns columns to be inserted * @param array $insertvalues values to be inserted * * @return integer */ protected function insertRecord( $type, $insertcolumns, $insertvalues ) { $default = $this->defaultValue; $suffix = $this->getInsertSuffix( $type ); $table = $this->esc( $type ); if ( count( $insertvalues ) > 0 && is_array( $insertvalues[0] ) && count( $insertvalues[0] ) > 0 ) { $insertSlots = array(); foreach ( $insertcolumns as $k => $v ) { $insertcolumns[$k] = $this->esc( $v ); if (isset(self::$sqlFilters['w'][$type][$v])) { $insertSlots[] = self::$sqlFilters['w'][$type][$v]; } else { $insertSlots[] = '?'; } } $insertSQL = "INSERT INTO $table ( id, " . implode( ',', $insertcolumns ) . " ) VALUES ( $default, " . implode( ',', $insertSlots ) . " ) $suffix"; $ids = array(); foreach ( $insertvalues as $i => $insertvalue ) { $ids[] = $this->adapter->getCell( $insertSQL, $insertvalue, $i ); } $result = count( $ids ) === 1 ? array_pop( $ids ) : $ids; } else { $result = $this->adapter->getCell( "INSERT INTO $table (id) VALUES($default) $suffix" ); } if ( $suffix ) return $result; $last_id = $this->adapter->getInsertID(); return $last_id; } /** * Checks table name or column name. * * @param string $table table string * * @return string */ protected function check( $struct ) { if ( !is_string( $struct ) || !preg_match( '/^[a-zA-Z0-9_]+$/', $struct ) ) { throw new RedException( 'Identifier does not conform to RedBeanPHP security policies.' ); } return $struct; } /** * Checks whether the specified type (i.e. table) already exists in the database. * Not part of the Object Database interface! * * @param string $table table name * * @return boolean */ public function tableExists( $table ) { $tables = $this->getTables(); return in_array( $table, $tables ); } /** * @see QueryWriter::glueSQLCondition */ public function glueSQLCondition( $sql, $glue = NULL ) { static $snippetCache = array(); if ( trim( $sql ) === '' ) { return $sql; } $key = $glue . '|' . $sql; if ( isset( $snippetCache[$key] ) ) { return $snippetCache[$key]; } $lsql = ltrim( $sql ); if ( preg_match( '/^(INNER|LEFT|RIGHT|JOIN|AND|OR|WHERE|ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', $lsql ) ) { if ( $glue === QueryWriter::C_GLUE_WHERE && stripos( $lsql, 'AND' ) === 0 ) { $snippetCache[$key] = ' WHERE ' . substr( $lsql, 3 ); } else { $snippetCache[$key] = $sql; } } else { $snippetCache[$key] = ( ( $glue === QueryWriter::C_GLUE_AND ) ? ' AND ' : ' WHERE ') . $sql; } return $snippetCache[$key]; } /** * @see QueryWriter::glueLimitOne */ public function glueLimitOne( $sql = '') { return ( strpos( strtoupper( $sql ), 'LIMIT' ) === FALSE ) ? ( $sql . ' LIMIT 1 ' ) : $sql; } /** * @see QueryWriter::esc */ public function esc( $dbStructure, $dontQuote = FALSE ) { $this->check( $dbStructure ); return ( $dontQuote ) ? $dbStructure : $this->quoteCharacter . $dbStructure . $this->quoteCharacter; } /** * @see QueryWriter::addColumn */ public function addColumn( $type, $column, $field ) { $table = $type; $type = $field; $table = $this->esc( $table ); $column = $this->esc( $column ); $type = ( isset( $this->typeno_sqltype[$type] ) ) ? $this->typeno_sqltype[$type] : ''; $this->adapter->exec( "ALTER TABLE $table ADD $column $type " ); } /** * @see QueryWriter::updateRecord */ public function updateRecord( $type, $updatevalues, $id = NULL ) { $table = $type; if ( !$id ) { $insertcolumns = $insertvalues = array(); foreach ( $updatevalues as $pair ) { $insertcolumns[] = $pair['property']; $insertvalues[] = $pair['value']; } //Otherwise psql returns string while MySQL/SQLite return numeric causing problems with additions (array_diff) return (string) $this->insertRecord( $table, $insertcolumns, array( $insertvalues ) ); } if ( $id && !count( $updatevalues ) ) { return $id; } $table = $this->esc( $table ); $sql = "UPDATE $table SET "; $p = $v = array(); foreach ( $updatevalues as $uv ) { if ( isset( self::$sqlFilters['w'][$type][$uv['property']] ) ) { $p[] = " {$this->esc( $uv["property"] )} = ". self::$sqlFilters['w'][$type][$uv['property']]; } else { $p[] = " {$this->esc( $uv["property"] )} = ? "; } $v[] = $uv['value']; } $sql .= implode( ',', $p ) . ' WHERE id = ? '; $v[] = $id; $this->adapter->exec( $sql, $v ); return $id; } /** * @see QueryWriter::writeJoin */ public function writeJoin( $type, $targetType, $leftRight = 'LEFT' ) { if ( $leftRight !== 'LEFT' && $leftRight !== 'RIGHT' && $leftRight !== 'INNER' ) throw new RedException( 'Invalid JOIN.' ); $table = $this->esc( $type ); $targetTable = $this->esc( $targetType ); $field = $this->esc( $targetType, TRUE ); return " {$leftRight} JOIN {$targetTable} ON {$targetTable}.id = {$table}.{$field}_id "; } /** * Sets an SQL snippet to be used for the next queryRecord() operation. * A select snippet will be inserted at the end of the SQL select statement and * can be used to modify SQL-select commands to enable locking, for instance * using the 'FOR UPDATE' snippet (this will generate an SQL query like: * 'SELECT * FROM ... FOR UPDATE'. After the query has been executed the * SQL snippet will be erased. Note that only the first upcoming direct or * indirect invocation of queryRecord() through batch(), find() or load() * will be affected. The SQL snippet will be cached. * * @param string $sql SQL snippet to use in SELECT statement. * * return self */ public function setSQLSelectSnippet( $sqlSelectSnippet = '' ) { $this->sqlSelectSnippet = $sqlSelectSnippet; return $this; } /** * @see QueryWriter::queryRecord */ public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) { $addSql = $this->glueSQLCondition( $addSql, ( count($conditions) > 0) ? QueryWriter::C_GLUE_AND : NULL ); $key = NULL; if ( $this->flagUseCache ) { $key = $this->getCacheKey( array( $conditions, "$addSql {$this->sqlSelectSnippet}", $bindings, 'select' ) ); if ( $cached = $this->getCached( $type, $key ) ) { return $cached; } } $table = $this->esc( $type ); $sqlFilterStr = ''; if ( count( self::$sqlFilters ) ) { $sqlFilterStr = $this->getSQLFilterSnippet( $type ); } $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); $fieldSelection = ( self::$flagNarrowFieldMode ) ? "{$table}.*" : '*'; $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} {$this->sqlSelectSnippet} -- keep-cache"; $this->sqlSelectSnippet = ''; $rows = $this->adapter->get( $sql, $bindings ); if ( $this->flagUseCache && $key ) { $this->putResultInCache( $type, $key, $rows ); } return $rows; } /** * @see QueryWriter::queryRecordWithCursor */ public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() ) { $sql = $this->glueSQLCondition( $addSql, NULL ); $table = $this->esc( $type ); $sql = "SELECT {$table}.* FROM {$table} {$sql}"; return $this->adapter->getCursor( $sql, $bindings ); } /** * @see QueryWriter::queryRecordRelated */ public function queryRecordRelated( $sourceType, $destType, $linkIDs, $addSql = '', $bindings = array() ) { $addSql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE ); list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); $key = $this->getCacheKey( array( $sourceType, $destType, implode( ',', $linkIDs ), $addSql, $bindings ) ); if ( $this->flagUseCache && $cached = $this->getCached( $destType, $key ) ) { return $cached; } $inClause = $this->getParametersForInClause( $linkIDs, $bindings ); $sqlFilterStr = ''; if ( count( self::$sqlFilters ) ) { $sqlFilterStr = $this->getSQLFilterSnippet( $destType ); } if ( $sourceType === $destType ) { $inClause2 = $this->getParametersForInClause( $linkIDs, $bindings, count( $bindings ) ); //for some databases $sql = " SELECT {$destTable}.* {$sqlFilterStr} , COALESCE( NULLIF({$linkTable}.{$sourceCol}, {$destTable}.id), NULLIF({$linkTable}.{$destCol}, {$destTable}.id)) AS linked_by FROM {$linkTable} INNER JOIN {$destTable} ON ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) OR ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} IN ($inClause2) ) {$addSql} -- keep-cache"; $linkIDs = array_merge( $linkIDs, $linkIDs ); } else { $sql = " SELECT {$destTable}.* {$sqlFilterStr}, {$linkTable}.{$sourceCol} AS linked_by FROM {$linkTable} INNER JOIN {$destTable} ON ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) {$addSql} -- keep-cache"; } $bindings = array_merge( $linkIDs, $bindings ); $rows = $this->adapter->get( $sql, $bindings ); $this->putResultInCache( $destType, $key, $rows ); return $rows; } /** * @see QueryWriter::queryRecordLink */ public function queryRecordLink( $sourceType, $destType, $sourceID, $destID ) { list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); $key = $this->getCacheKey( array( $sourceType, $destType, $sourceID, $destID ) ); if ( $this->flagUseCache && $cached = $this->getCached( $linkTable, $key ) ) { return $cached; } $sqlFilterStr = ''; if ( count( self::$sqlFilters ) ) { $sqlFilterStr = $this->getSQLFilterSnippet( $destType ); } if ( $sourceTable === $destTable ) { $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable} WHERE ( {$sourceCol} = ? AND {$destCol} = ? ) OR ( {$destCol} = ? AND {$sourceCol} = ? ) -- keep-cache"; $row = $this->adapter->getRow( $sql, array( $sourceID, $destID, $sourceID, $destID ) ); } else { $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable} WHERE {$sourceCol} = ? AND {$destCol} = ? -- keep-cache"; $row = $this->adapter->getRow( $sql, array( $sourceID, $destID ) ); } $this->putResultInCache( $linkTable, $key, $row ); return $row; } /** * @see QueryWriter::queryTagged */ public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ) { $assocType = $this->getAssocTable( array( $type, 'tag' ) ); $assocTable = $this->esc( $assocType ); $assocField = $type . '_id'; $table = $this->esc( $type ); $slots = implode( ',', array_fill( 0, count( $tagList ), '?' ) ); $score = ( $all ) ? count( $tagList ) : 1; $sql = " SELECT {$table}.*, count({$table}.id) FROM {$table} INNER JOIN {$assocTable} ON {$assocField} = {$table}.id INNER JOIN tag ON {$assocTable}.tag_id = tag.id WHERE tag.title IN ({$slots}) GROUP BY {$table}.id HAVING count({$table}.id) >= ? {$addSql} "; $bindings = array_merge( $tagList, array( $score ), $bindings ); $rows = $this->adapter->get( $sql, $bindings ); return $rows; } /** * @see QueryWriter::queryRecordCount */ public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) { $addSql = $this->glueSQLCondition( $addSql ); $table = $this->esc( $type ); $this->updateCache(); //check if cache chain has been broken $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); $sql = "SELECT COUNT(*) FROM {$table} {$sql} -- keep-cache"; return (int) $this->adapter->getCell( $sql, $bindings ); } /** * @see QueryWriter::queryRecordCountRelated */ public function queryRecordCountRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() ) { list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); $this->updateCache(); //check if cache chain has been broken if ( $sourceType === $destType ) { $sql = " SELECT COUNT(*) FROM {$linkTable} INNER JOIN {$destTable} ON ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) OR ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} = ? ) {$addSql} -- keep-cache"; $bindings = array_merge( array( $linkID, $linkID ), $bindings ); } else { $sql = " SELECT COUNT(*) FROM {$linkTable} INNER JOIN {$destTable} ON ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) {$addSql} -- keep-cache"; $bindings = array_merge( array( $linkID ), $bindings ); } return (int) $this->adapter->getCell( $sql, $bindings ); } /** * @see QueryWriter::deleteRecord */ public function deleteRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) { $addSql = $this->glueSQLCondition( $addSql ); $table = $this->esc( $type ); $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); $sql = "DELETE FROM {$table} {$sql}"; $this->adapter->exec( $sql, $bindings ); } /** * @see QueryWriter::deleteRelations */ public function deleteRelations( $sourceType, $destType, $sourceID ) { list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); if ( $sourceTable === $destTable ) { $sql = "DELETE FROM {$linkTable} WHERE ( {$sourceCol} = ? ) OR ( {$destCol} = ? ) "; $this->adapter->exec( $sql, array( $sourceID, $sourceID ) ); } else { $sql = "DELETE FROM {$linkTable} WHERE {$sourceCol} = ? "; $this->adapter->exec( $sql, array( $sourceID ) ); } } /** * @see QueryWriter::widenColumn */ public function widenColumn( $type, $property, $dataType ) { if ( !isset($this->typeno_sqltype[$dataType]) ) return FALSE; $table = $this->esc( $type ); $column = $this->esc( $property ); $newType = $this->typeno_sqltype[$dataType]; $this->adapter->exec( "ALTER TABLE $table CHANGE $column $column $newType " ); return TRUE; } /** * @see QueryWriter::wipe */ public function wipe( $type ) { $table = $this->esc( $type ); $this->adapter->exec( "TRUNCATE $table " ); } /** * @see QueryWriter::renameAssocTable */ public function renameAssocTable( $from, $to = NULL ) { self::renameAssociation( $from, $to ); } /** * @see QueryWriter::getAssocTable */ public function getAssocTable( $types ) { return self::getAssocTableFormat( $types ); } /** * Turns caching on or off. Default: off. * If caching is turned on retrieval queries fired after eachother will * use a result row cache. * * @param boolean * * @return void */ public function setUseCache( $yesNo ) { $this->flushCache(); $this->flagUseCache = (bool) $yesNo; } /** * Flushes the Query Writer Cache. * Clears the internal query cache array and returns its overall * size. * * @return integer */ public function flushCache( $newMaxCacheSizePerType = NULL ) { if ( !is_null( $newMaxCacheSizePerType ) && $newMaxCacheSizePerType > 0 ) { $this->maxCacheSizePerType = $newMaxCacheSizePerType; } $count = count( $this->cache, COUNT_RECURSIVE ); $this->cache = array(); return $count; } /** * @deprecated Use esc() instead. * * @param string $column column to be escaped * @param boolean $noQuotes omit quotes * * @return string */ public function safeColumn( $column, $noQuotes = FALSE ) { return $this->esc( $column, $noQuotes ); } /** * @deprecated Use esc() instead. * * @param string $table table to be escaped * @param boolean $noQuotes omit quotes * * @return string */ public function safeTable( $table, $noQuotes = FALSE ) { return $this->esc( $table, $noQuotes ); } /** * @see QueryWriter::inferFetchType */ public function inferFetchType( $type, $property ) { $type = $this->esc( $type, TRUE ); $field = $this->esc( $property, TRUE ) . '_id'; $keys = $this->getKeyMapForType( $type ); foreach( $keys as $key ) { if ( $key['from'] === $field ) return $key['table']; } return NULL; } /** * @see QueryWriter::addUniqueConstraint */ public function addUniqueIndex( $type, $properties ) { return $this->addUniqueConstraint( $type, $properties ); } } } namespace RedBeanPHP\QueryWriter { use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\Adapter\DBAdapter as DBAdapter; use RedBeanPHP\Adapter as Adapter; use RedBeanPHP\RedException\SQL as SQLException; /** * RedBeanPHP MySQLWriter. * This is a QueryWriter class for RedBeanPHP. * This QueryWriter provides support for the MySQL/MariaDB database platform. * * @file RedBeanPHP/QueryWriter/MySQL.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @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. */ class MySQL extends AQueryWriter implements QueryWriter { /** * Data types */ const C_DATATYPE_BOOL = 0; const C_DATATYPE_UINT32 = 2; const C_DATATYPE_DOUBLE = 3; const C_DATATYPE_TEXT7 = 4; //InnoDB cant index varchar(255) utf8mb4 - so keep 191 as long as possible const C_DATATYPE_TEXT8 = 5; const C_DATATYPE_TEXT16 = 6; const C_DATATYPE_TEXT32 = 7; const C_DATATYPE_SPECIAL_DATE = 80; const C_DATATYPE_SPECIAL_DATETIME = 81; const C_DATATYPE_SPECIAL_POINT = 90; const C_DATATYPE_SPECIAL_LINESTRING = 91; const C_DATATYPE_SPECIAL_POLYGON = 92; const C_DATATYPE_SPECIAL_MONEY = 93; const C_DATATYPE_SPECIAL_JSON = 94; //JSON support (only manual) const C_DATATYPE_SPECIFIED = 99; /** * @var DBAdapter */ protected $adapter; /** * @var string */ protected $quoteCharacter = '`'; /** * @see AQueryWriter::getKeyMapForType */ protected function getKeyMapForType( $type ) { $databaseName = $this->adapter->getCell('SELECT DATABASE()'); $table = $this->esc( $type, TRUE ); $keys = $this->adapter->get(' SELECT information_schema.key_column_usage.constraint_name AS `name`, information_schema.key_column_usage.referenced_table_name AS `table`, information_schema.key_column_usage.column_name AS `from`, information_schema.key_column_usage.referenced_column_name AS `to`, information_schema.referential_constraints.update_rule AS `on_update`, information_schema.referential_constraints.delete_rule AS `on_delete` FROM information_schema.key_column_usage INNER JOIN information_schema.referential_constraints ON information_schema.referential_constraints.constraint_name = information_schema.key_column_usage.constraint_name WHERE information_schema.key_column_usage.table_schema = :database AND information_schema.referential_constraints.constraint_schema = :database AND information_schema.key_column_usage.constraint_schema = :database AND information_schema.key_column_usage.table_name = :table AND information_schema.key_column_usage.constraint_name != \'PRIMARY\' AND information_schema.key_column_usage.referenced_table_name IS NOT NULL ', array( ':database' => $databaseName, ':table' => $table ) ); $keyInfoList = array(); foreach ( $keys as $k ) { $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); $keyInfoList[$label] = array( 'name' => $k['name'], 'from' => $k['from'], 'table' => $k['table'], 'to' => $k['to'], 'on_update' => $k['on_update'], 'on_delete' => $k['on_delete'] ); } return $keyInfoList; } /** * Constructor * * @param Adapter $adapter Database Adapter */ public function __construct( Adapter $adapter ) { $this->typeno_sqltype = array( MySQL::C_DATATYPE_BOOL => ' TINYINT(1) UNSIGNED ', MySQL::C_DATATYPE_UINT32 => ' INT(11) UNSIGNED ', MySQL::C_DATATYPE_DOUBLE => ' DOUBLE ', MySQL::C_DATATYPE_TEXT7 => ' VARCHAR(191) ', MYSQL::C_DATATYPE_TEXT8 => ' VARCHAR(255) ', MySQL::C_DATATYPE_TEXT16 => ' TEXT ', MySQL::C_DATATYPE_TEXT32 => ' LONGTEXT ', MySQL::C_DATATYPE_SPECIAL_DATE => ' DATE ', MySQL::C_DATATYPE_SPECIAL_DATETIME => ' DATETIME ', MySQL::C_DATATYPE_SPECIAL_POINT => ' POINT ', MySQL::C_DATATYPE_SPECIAL_LINESTRING => ' LINESTRING ', MySQL::C_DATATYPE_SPECIAL_POLYGON => ' POLYGON ', MySQL::C_DATATYPE_SPECIAL_MONEY => ' DECIMAL(10,2) ', MYSQL::C_DATATYPE_SPECIAL_JSON => ' JSON ' ); $this->sqltype_typeno = array(); foreach ( $this->typeno_sqltype as $k => $v ) { $this->sqltype_typeno[trim( strtolower( $v ) )] = $k; } $this->adapter = $adapter; $this->encoding = $this->adapter->getDatabase()->getMysqlEncoding(); } /** * This method returns the datatype to be used for primary key IDS and * foreign keys. Returns one if the data type constants. * * @return integer */ public function getTypeForID() { return self::C_DATATYPE_UINT32; } /** * @see QueryWriter::getTables */ public function getTables() { return $this->adapter->getCol( 'show tables' ); } /** * @see QueryWriter::createTable */ public function createTable( $table ) { $table = $this->esc( $table ); $charset_collate = $this->adapter->getDatabase()->getMysqlEncoding( TRUE ); $charset = $charset_collate['charset']; $collate = $charset_collate['collate']; $sql = "CREATE TABLE $table (id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY ( id )) ENGINE = InnoDB DEFAULT CHARSET={$charset} COLLATE={$collate} "; $this->adapter->exec( $sql ); } /** * @see QueryWriter::getColumns */ public function getColumns( $table ) { $columnsRaw = $this->adapter->get( "DESCRIBE " . $this->esc( $table ) ); $columns = array(); foreach ( $columnsRaw as $r ) { $columns[$r['Field']] = $r['Type']; } return $columns; } /** * @see QueryWriter::scanType */ public function scanType( $value, $flagSpecial = FALSE ) { $this->svalue = $value; if ( is_null( $value ) ) return MySQL::C_DATATYPE_BOOL; if ( $value === INF ) return MySQL::C_DATATYPE_TEXT7; if ( $flagSpecial ) { if ( preg_match( '/^-?\d+\.\d{2}$/', $value ) ) { return MySQL::C_DATATYPE_SPECIAL_MONEY; } if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) { return MySQL::C_DATATYPE_SPECIAL_DATE; } if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d$/', $value ) ) { return MySQL::C_DATATYPE_SPECIAL_DATETIME; } if ( preg_match( '/^POINT\(/', $value ) ) { return MySQL::C_DATATYPE_SPECIAL_POINT; } if ( preg_match( '/^LINESTRING\(/', $value ) ) { return MySQL::C_DATATYPE_SPECIAL_LINESTRING; } if ( preg_match( '/^POLYGON\(/', $value ) ) { return MySQL::C_DATATYPE_SPECIAL_POLYGON; } if ( self::$flagUseJSONColumns && $this->isJSON( $value ) ) { return self::C_DATATYPE_SPECIAL_JSON; } } //setter turns TRUE FALSE into 0 and 1 because database has no real bools (TRUE and FALSE only for test?). if ( $value === FALSE || $value === TRUE || $value === '0' || $value === '1' ) { return MySQL::C_DATATYPE_BOOL; } if ( is_float( $value ) ) return self::C_DATATYPE_DOUBLE; if ( !$this->startsWithZeros( $value ) ) { if ( is_numeric( $value ) && ( floor( $value ) == $value ) && $value >= 0 && $value <= 4294967295 ) { return MySQL::C_DATATYPE_UINT32; } if ( is_numeric( $value ) ) { return MySQL::C_DATATYPE_DOUBLE; } } if ( mb_strlen( $value, 'UTF-8' ) <= 191 ) { return MySQL::C_DATATYPE_TEXT7; } if ( mb_strlen( $value, 'UTF-8' ) <= 255 ) { return MySQL::C_DATATYPE_TEXT8; } if ( mb_strlen( $value, 'UTF-8' ) <= 65535 ) { return MySQL::C_DATATYPE_TEXT16; } return MySQL::C_DATATYPE_TEXT32; } /** * @see QueryWriter::code */ public function code( $typedescription, $includeSpecials = FALSE ) { if ( isset( $this->sqltype_typeno[$typedescription] ) ) { $r = $this->sqltype_typeno[$typedescription]; } else { $r = self::C_DATATYPE_SPECIFIED; } if ( $includeSpecials ) { return $r; } if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) { return self::C_DATATYPE_SPECIFIED; } return $r; } /** * @see QueryWriter::addUniqueIndex */ public function addUniqueConstraint( $type, $properties ) { $tableNoQ = $this->esc( $type, TRUE ); $columns = array(); foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); $table = $this->esc( $type ); sort( $columns ); // Else we get multiple indexes due to order-effects $name = 'UQ_' . sha1( implode( ',', $columns ) ); try { $sql = "ALTER TABLE $table ADD UNIQUE INDEX $name (" . implode( ',', $columns ) . ")"; $this->adapter->exec( $sql ); } catch ( SQLException $e ) { //do nothing, dont use alter table ignore, this will delete duplicate records in 3-ways! return FALSE; } return TRUE; } /** * @see QueryWriter::addIndex */ public function addIndex( $type, $name, $property ) { try { $table = $this->esc( $type ); $name = preg_replace( '/\W/', '', $name ); $column = $this->esc( $property ); $this->adapter->exec( "CREATE INDEX $name ON $table ($column) " ); return TRUE; } catch ( SQLException $e ) { return FALSE; } } /** * @see QueryWriter::addFK * @return bool */ public function addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE ) { $table = $this->esc( $type ); $targetTable = $this->esc( $targetType ); $targetTableNoQ = $this->esc( $targetType, TRUE ); $field = $this->esc( $property ); $fieldNoQ = $this->esc( $property, TRUE ); $targetField = $this->esc( $targetProperty ); $targetFieldNoQ = $this->esc( $targetProperty, TRUE ); $tableNoQ = $this->esc( $type, TRUE ); $fieldNoQ = $this->esc( $property, TRUE ); if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $fieldNoQ ) ) ) return FALSE; //Widen the column if it's incapable of representing a foreign key (at least INT). $columns = $this->getColumns( $tableNoQ ); $idType = $this->getTypeForID(); if ( $this->code( $columns[$fieldNoQ] ) !== $idType ) { $this->widenColumn( $type, $property, $idType ); } $fkName = 'fk_'.($tableNoQ.'_'.$fieldNoQ); $cName = 'c_'.$fkName; try { $this->adapter->exec( " ALTER TABLE {$table} ADD CONSTRAINT $cName FOREIGN KEY $fkName ( `{$fieldNoQ}` ) REFERENCES `{$targetTableNoQ}` (`{$targetFieldNoQ}`) ON DELETE " . ( $isDependent ? 'CASCADE' : 'SET NULL' ) . ' ON UPDATE '.( $isDependent ? 'CASCADE' : 'SET NULL' ).';'); } catch ( SQLException $e ) { // Failure of fk-constraints is not a problem } return true; } /** * @see QueryWriter::sqlStateIn */ public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) { $stateMap = array( '42S02' => QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, '42S22' => QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, '23000' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION, ); if ( $state == 'HY000' && !empty( $extraDriverDetails[1] ) ) { $driverCode = $extraDriverDetails[1]; if ( $driverCode == '1205' && in_array( QueryWriter::C_SQLSTATE_LOCK_TIMEOUT, $list ) ) { return true; } } return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); } /** * @see QueryWriter::wipeAll */ public function wipeAll() { $this->adapter->exec( 'SET FOREIGN_KEY_CHECKS = 0;' ); foreach ( $this->getTables() as $t ) { try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { } try { $this->adapter->exec( "DROP VIEW IF EXISTS `$t`" ); } catch ( SQLException $e ) { } } $this->adapter->exec( 'SET FOREIGN_KEY_CHECKS = 1;' ); } } } namespace RedBeanPHP\QueryWriter { use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\Adapter\DBAdapter as DBAdapter; use RedBeanPHP\Adapter as Adapter; use RedBeanPHP\RedException\SQL as SQLException; /** * RedBeanPHP SQLiteWriter with support for SQLite types * This is a QueryWriter class for RedBeanPHP. * This QueryWriter provides support for the SQLite database platform. * * @file RedBeanPHP/QueryWriter/SQLiteT.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @copyright * (c) copyright 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. */ class SQLiteT extends AQueryWriter implements QueryWriter { /** * Data types */ const C_DATATYPE_INTEGER = 0; const C_DATATYPE_NUMERIC = 1; const C_DATATYPE_TEXT = 2; const C_DATATYPE_SPECIFIED = 99; /** * @var DBAdapter */ protected $adapter; /** * @var string */ protected $quoteCharacter = '`'; /** * Gets all information about a table (from a type). * * Format: * array( * name => name of the table * columns => array( name => datatype ) * indexes => array() raw index information rows from PRAGMA query * keys => array() raw key information rows from PRAGMA query * ) * * @param string $type type you want to get info of * * @return array */ protected function getTable( $type ) { $tableName = $this->esc( $type, TRUE ); $columns = $this->getColumns( $type ); $indexes = $this->getIndexes( $type ); $keys = $this->getKeyMapForType( $type ); $table = array( 'columns' => $columns, 'indexes' => $indexes, 'keys' => $keys, 'name' => $tableName ); $this->tableArchive[$tableName] = $table; return $table; } /** * Puts a table. Updates the table structure. * In SQLite we can't change columns, drop columns, change or add foreign keys so we * have a table-rebuild function. You simply load your table with getTable(), modify it and * then store it with putTable()... * * @param array $tableMap information array * * @return void */ protected function putTable( $tableMap ) { $table = $tableMap['name']; $q = array(); $q[] = "DROP TABLE IF EXISTS tmp_backup;"; $oldColumnNames = array_keys( $this->getColumns( $table ) ); foreach ( $oldColumnNames as $k => $v ) $oldColumnNames[$k] = "`$v`"; $q[] = "CREATE TEMPORARY TABLE tmp_backup(" . implode( ",", $oldColumnNames ) . ");"; $q[] = "INSERT INTO tmp_backup SELECT * FROM `$table`;"; $q[] = "PRAGMA foreign_keys = 0 "; $q[] = "DROP TABLE `$table`;"; $newTableDefStr = ''; foreach ( $tableMap['columns'] as $column => $type ) { if ( $column != 'id' ) { $newTableDefStr .= ",`$column` $type"; } } $fkDef = ''; foreach ( $tableMap['keys'] as $key ) { $fkDef .= ", FOREIGN KEY(`{$key['from']}`) REFERENCES `{$key['table']}`(`{$key['to']}`) ON DELETE {$key['on_delete']} ON UPDATE {$key['on_update']}"; } $q[] = "CREATE TABLE `$table` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT $newTableDefStr $fkDef );"; foreach ( $tableMap['indexes'] as $name => $index ) { if ( strpos( $name, 'UQ_' ) === 0 ) { $cols = explode( '__', substr( $name, strlen( 'UQ_' . $table ) ) ); foreach ( $cols as $k => $v ) $cols[$k] = "`$v`"; $q[] = "CREATE UNIQUE INDEX $name ON `$table` (" . implode( ',', $cols ) . ")"; } else $q[] = "CREATE INDEX $name ON `$table` ({$index['name']}) "; } $q[] = "INSERT INTO `$table` SELECT * FROM tmp_backup;"; $q[] = "DROP TABLE tmp_backup;"; $q[] = "PRAGMA foreign_keys = 1 "; foreach ( $q as $sq ) $this->adapter->exec( $sq ); } /** * Returns the an array describing the indexes for type $type. * * @param string $type type to describe indexes of * * @return array */ protected function getIndexes( $type ) { $table = $this->esc( $type, TRUE ); $indexes = $this->adapter->get( "PRAGMA index_list('$table')" ); $indexInfoList = array(); foreach ( $indexes as $i ) { $indexInfoList[$i['name']] = $this->adapter->getRow( "PRAGMA index_info('{$i['name']}') " ); $indexInfoList[$i['name']]['unique'] = $i['unique']; } return $indexInfoList; } /** * Adds a foreign key to a type. * Note: cant put this in try-catch because that can hide the fact * that database has been damaged. * * @param string $type type you want to modify table of * @param string $targetType target type * @param string $field field of the type that needs to get the fk * @param string $targetField field where the fk needs to point to * @param integer $buildopt 0 = NO ACTION, 1 = ON DELETE CASCADE * * @return boolean */ protected function buildFK( $type, $targetType, $property, $targetProperty, $constraint = FALSE ) { $table = $this->esc( $type, TRUE ); $targetTable = $this->esc( $targetType, TRUE ); $column = $this->esc( $property, TRUE ); $targetColumn = $this->esc( $targetProperty, TRUE ); $tables = $this->getTables(); if ( !in_array( $targetTable, $tables ) ) return FALSE; if ( !is_null( $this->getForeignKeyForTypeProperty( $table, $column ) ) ) return FALSE; $t = $this->getTable( $table ); $consSQL = ( $constraint ? 'CASCADE' : 'SET NULL' ); $label = 'from_' . $column . '_to_table_' . $targetTable . '_col_' . $targetColumn; $t['keys'][$label] = array( 'table' => $targetTable, 'from' => $column, 'to' => $targetColumn, 'on_update' => $consSQL, 'on_delete' => $consSQL ); $this->putTable( $t ); return TRUE; } /** * @see AQueryWriter::getKeyMapForType */ protected function getKeyMapForType( $type ) { $table = $this->esc( $type, TRUE ); $keys = $this->adapter->get( "PRAGMA foreign_key_list('$table')" ); $keyInfoList = array(); foreach ( $keys as $k ) { $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); $keyInfoList[$label] = array( 'name' => $label, 'from' => $k['from'], 'table' => $k['table'], 'to' => $k['to'], 'on_update' => $k['on_update'], 'on_delete' => $k['on_delete'] ); } return $keyInfoList; } /** * Constructor * * @param Adapter $adapter Database Adapter */ public function __construct( Adapter $adapter ) { $this->typeno_sqltype = array( SQLiteT::C_DATATYPE_INTEGER => 'INTEGER', SQLiteT::C_DATATYPE_NUMERIC => 'NUMERIC', SQLiteT::C_DATATYPE_TEXT => 'TEXT', ); $this->sqltype_typeno = array(); foreach ( $this->typeno_sqltype as $k => $v ) { $this->sqltype_typeno[$v] = $k; } $this->adapter = $adapter; $this->adapter->setOption( 'setInitQuery', ' PRAGMA foreign_keys = 1 ' ); } /** * This method returns the datatype to be used for primary key IDS and * foreign keys. Returns one if the data type constants. * * @return integer $const data type to be used for IDS. */ public function getTypeForID() { return self::C_DATATYPE_INTEGER; } /** * @see QueryWriter::scanType */ public function scanType( $value, $flagSpecial = FALSE ) { $this->svalue = $value; if ( $value === NULL ) return self::C_DATATYPE_INTEGER; if ( $value === INF ) return self::C_DATATYPE_TEXT; if ( $this->startsWithZeros( $value ) ) return self::C_DATATYPE_TEXT; if ( $value === TRUE || $value === FALSE ) return self::C_DATATYPE_INTEGER; if ( is_numeric( $value ) && ( intval( $value ) == $value ) && $value < 2147483648 && $value > -2147483648 ) return self::C_DATATYPE_INTEGER; if ( ( is_numeric( $value ) && $value < 2147483648 && $value > -2147483648) || preg_match( '/\d{4}\-\d\d\-\d\d/', $value ) || preg_match( '/\d{4}\-\d\d\-\d\d\s\d\d:\d\d:\d\d/', $value ) ) { return self::C_DATATYPE_NUMERIC; } return self::C_DATATYPE_TEXT; } /** * @see QueryWriter::addColumn */ public function addColumn( $table, $column, $type ) { $column = $this->check( $column ); $table = $this->check( $table ); $type = $this->typeno_sqltype[$type]; $this->adapter->exec( "ALTER TABLE `$table` ADD `$column` $type " ); } /** * @see QueryWriter::code */ public function code( $typedescription, $includeSpecials = FALSE ) { $r = ( ( isset( $this->sqltype_typeno[$typedescription] ) ) ? $this->sqltype_typeno[$typedescription] : 99 ); return $r; } /** * @see QueryWriter::widenColumn */ public function widenColumn( $type, $column, $datatype ) { $t = $this->getTable( $type ); $t['columns'][$column] = $this->typeno_sqltype[$datatype]; $this->putTable( $t ); } /** * @see QueryWriter::getTables(); */ public function getTables() { return $this->adapter->getCol( "SELECT name FROM sqlite_master WHERE type='table' AND name!='sqlite_sequence';" ); } /** * @see QueryWriter::createTable */ public function createTable( $table ) { $table = $this->esc( $table ); $sql = "CREATE TABLE $table ( id INTEGER PRIMARY KEY AUTOINCREMENT ) "; $this->adapter->exec( $sql ); } /** * @see QueryWriter::getColumns */ public function getColumns( $table ) { $table = $this->esc( $table, TRUE ); $columnsRaw = $this->adapter->get( "PRAGMA table_info('$table')" ); $columns = array(); foreach ( $columnsRaw as $r ) $columns[$r['name']] = $r['type']; return $columns; } /** * @see QueryWriter::addUniqueIndex */ public function addUniqueConstraint( $type, $properties ) { $tableNoQ = $this->esc( $type, TRUE ); $name = 'UQ_' . $this->esc( $type, TRUE ) . implode( '__', $properties ); $t = $this->getTable( $type ); $t['indexes'][$name] = array( 'name' => $name ); try { $this->putTable( $t ); } catch( SQLException $e ) { return FALSE; } return TRUE; } /** * @see QueryWriter::sqlStateIn */ public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) { $stateMap = array( '23000' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION ); if ( $state == 'HY000' && isset($extraDriverDetails[1]) && $extraDriverDetails[1] == 1 && ( in_array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, $list ) || in_array( QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, $list ) )) { return TRUE; } return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); } /** * @see QueryWriter::addIndex */ public function addIndex( $type, $name, $column ) { $columns = $this->getColumns( $type ); if ( !isset( $columns[$column] ) ) return FALSE; $table = $this->esc( $type ); $name = preg_replace( '/\W/', '', $name ); $column = $this->esc( $column, TRUE ); try { $t = $this->getTable( $type ); $t['indexes'][$name] = array( 'name' => $column ); $this->putTable( $t ); return TRUE; } catch( SQLException $exception ) { return FALSE; } } /** * @see QueryWriter::wipe */ public function wipe( $type ) { $table = $this->esc( $type ); $this->adapter->exec( "DELETE FROM $table " ); } /** * @see QueryWriter::addFK */ public function addFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ) { return $this->buildFK( $type, $targetType, $property, $targetProperty, $isDep ); } /** * @see QueryWriter::wipeAll */ public function wipeAll() { $this->adapter->exec( 'PRAGMA foreign_keys = 0 ' ); foreach ( $this->getTables() as $t ) { try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { } try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { } } $this->adapter->exec( 'PRAGMA foreign_keys = 1 ' ); } } } namespace RedBeanPHP\QueryWriter { use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\Adapter\DBAdapter as DBAdapter; use RedBeanPHP\Adapter as Adapter; use RedBeanPHP\RedException\SQL as SQLException; /** * RedBeanPHP PostgreSQL Query Writer. * This is a QueryWriter class for RedBeanPHP. * This QueryWriter provides support for the PostgreSQL database platform. * * @file RedBeanPHP/QueryWriter/PostgreSQL.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @copyright * (c) copyright 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. */ class PostgreSQL extends AQueryWriter implements QueryWriter { /** * Data types */ const C_DATATYPE_INTEGER = 0; const C_DATATYPE_DOUBLE = 1; const C_DATATYPE_TEXT = 3; const C_DATATYPE_SPECIAL_DATE = 80; const C_DATATYPE_SPECIAL_DATETIME = 81; const C_DATATYPE_SPECIAL_POINT = 90; const C_DATATYPE_SPECIAL_LSEG = 91; const C_DATATYPE_SPECIAL_CIRCLE = 92; const C_DATATYPE_SPECIAL_MONEY = 93; const C_DATATYPE_SPECIAL_POLYGON = 94; const C_DATATYPE_SPECIAL_MONEY2 = 95; //Numbers only money, i.e. fixed point numeric const C_DATATYPE_SPECIAL_JSON = 96; //JSON support (only manual) const C_DATATYPE_SPECIFIED = 99; /** * @var DBAdapter */ protected $adapter; /** * @var string */ protected $quoteCharacter = '"'; /** * @var string */ protected $defaultValue = 'DEFAULT'; /** * Returns the insert suffix SQL Snippet * * @param string $table table * * @return string $sql SQL Snippet */ protected function getInsertSuffix( $table ) { return 'RETURNING id '; } /** * @see AQueryWriter::getKeyMapForType */ protected function getKeyMapForType( $type ) { $table = $this->esc( $type, TRUE ); $keys = $this->adapter->get( ' SELECT information_schema.key_column_usage.constraint_name AS "name", information_schema.key_column_usage.column_name AS "from", information_schema.constraint_table_usage.table_name AS "table", information_schema.constraint_column_usage.column_name AS "to", information_schema.referential_constraints.update_rule AS "on_update", information_schema.referential_constraints.delete_rule AS "on_delete" FROM information_schema.key_column_usage INNER JOIN information_schema.constraint_table_usage ON ( information_schema.key_column_usage.constraint_name = information_schema.constraint_table_usage.constraint_name AND information_schema.key_column_usage.constraint_schema = information_schema.constraint_table_usage.constraint_schema AND information_schema.key_column_usage.constraint_catalog = information_schema.constraint_table_usage.constraint_catalog ) INNER JOIN information_schema.constraint_column_usage ON ( information_schema.key_column_usage.constraint_name = information_schema.constraint_column_usage.constraint_name AND information_schema.key_column_usage.constraint_schema = information_schema.constraint_column_usage.constraint_schema AND information_schema.key_column_usage.constraint_catalog = information_schema.constraint_column_usage.constraint_catalog ) INNER JOIN information_schema.referential_constraints ON ( information_schema.key_column_usage.constraint_name = information_schema.referential_constraints.constraint_name AND information_schema.key_column_usage.constraint_schema = information_schema.referential_constraints.constraint_schema AND information_schema.key_column_usage.constraint_catalog = information_schema.referential_constraints.constraint_catalog ) WHERE information_schema.key_column_usage.table_catalog = current_database() AND information_schema.key_column_usage.table_schema = ANY( current_schemas( FALSE ) ) AND information_schema.key_column_usage.table_name = ? ', array( $type ) ); $keyInfoList = array(); foreach ( $keys as $k ) { $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); $keyInfoList[$label] = array( 'name' => $k['name'], 'from' => $k['from'], 'table' => $k['table'], 'to' => $k['to'], 'on_update' => $k['on_update'], 'on_delete' => $k['on_delete'] ); } return $keyInfoList; } /** * Constructor * * @param Adapter $adapter Database Adapter */ public function __construct( Adapter $adapter ) { $this->typeno_sqltype = array( self::C_DATATYPE_INTEGER => ' integer ', self::C_DATATYPE_DOUBLE => ' double precision ', self::C_DATATYPE_TEXT => ' text ', self::C_DATATYPE_SPECIAL_DATE => ' date ', self::C_DATATYPE_SPECIAL_DATETIME => ' timestamp without time zone ', self::C_DATATYPE_SPECIAL_POINT => ' point ', self::C_DATATYPE_SPECIAL_LSEG => ' lseg ', self::C_DATATYPE_SPECIAL_CIRCLE => ' circle ', self::C_DATATYPE_SPECIAL_MONEY => ' money ', self::C_DATATYPE_SPECIAL_MONEY2 => ' numeric(10,2) ', self::C_DATATYPE_SPECIAL_POLYGON => ' polygon ', self::C_DATATYPE_SPECIAL_JSON => ' json ', ); $this->sqltype_typeno = array(); foreach ( $this->typeno_sqltype as $k => $v ) { $this->sqltype_typeno[trim( strtolower( $v ) )] = $k; } $this->adapter = $adapter; } /** * This method returns the datatype to be used for primary key IDS and * foreign keys. Returns one if the data type constants. * * @return integer */ public function getTypeForID() { return self::C_DATATYPE_INTEGER; } /** * @see QueryWriter::getTables */ public function getTables() { return $this->adapter->getCol( 'SELECT table_name FROM information_schema.tables WHERE table_schema = ANY( current_schemas( FALSE ) )' ); } /** * @see QueryWriter::createTable */ public function createTable( $table ) { $table = $this->esc( $table ); $this->adapter->exec( " CREATE TABLE $table (id SERIAL PRIMARY KEY); " ); } /** * @see QueryWriter::getColumns */ public function getColumns( $table ) { $table = $this->esc( $table, TRUE ); $columnsRaw = $this->adapter->get( "SELECT column_name, data_type FROM information_schema.columns WHERE table_name='$table' AND table_schema = ANY( current_schemas( FALSE ) )" ); $columns = array(); foreach ( $columnsRaw as $r ) { $columns[$r['column_name']] = $r['data_type']; } return $columns; } /** * @see QueryWriter::scanType */ public function scanType( $value, $flagSpecial = FALSE ) { $this->svalue = $value; if ( $value === INF ) return self::C_DATATYPE_TEXT; if ( $flagSpecial && $value ) { if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) { return PostgreSQL::C_DATATYPE_SPECIAL_DATE; } if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d(\.\d{1,6})?$/', $value ) ) { return PostgreSQL::C_DATATYPE_SPECIAL_DATETIME; } if ( preg_match( '/^\([\d\.]+,[\d\.]+\)$/', $value ) ) { return PostgreSQL::C_DATATYPE_SPECIAL_POINT; } if ( preg_match( '/^\[\([\d\.]+,[\d\.]+\),\([\d\.]+,[\d\.]+\)\]$/', $value ) ) { return PostgreSQL::C_DATATYPE_SPECIAL_LSEG; } if ( preg_match( '/^\<\([\d\.]+,[\d\.]+\),[\d\.]+\>$/', $value ) ) { return PostgreSQL::C_DATATYPE_SPECIAL_CIRCLE; } if ( preg_match( '/^\((\([\d\.]+,[\d\.]+\),?)+\)$/', $value ) ) { return PostgreSQL::C_DATATYPE_SPECIAL_POLYGON; } if ( preg_match( '/^\-?(\$|€|¥|£)[\d,\.]+$/', $value ) ) { return PostgreSQL::C_DATATYPE_SPECIAL_MONEY; } if ( preg_match( '/^-?\d+\.\d{2}$/', $value ) ) { return PostgreSQL::C_DATATYPE_SPECIAL_MONEY2; } if ( self::$flagUseJSONColumns && $this->isJSON( $value ) ) { return self::C_DATATYPE_SPECIAL_JSON; } } if ( is_float( $value ) ) return self::C_DATATYPE_DOUBLE; if ( $this->startsWithZeros( $value ) ) return self::C_DATATYPE_TEXT; if ( $value === FALSE || $value === TRUE || $value === NULL || ( is_numeric( $value ) && AQueryWriter::canBeTreatedAsInt( $value ) && $value < 2147483648 && $value > -2147483648 ) ) { return self::C_DATATYPE_INTEGER; } elseif ( is_numeric( $value ) ) { return self::C_DATATYPE_DOUBLE; } else { return self::C_DATATYPE_TEXT; } } /** * @see QueryWriter::code */ public function code( $typedescription, $includeSpecials = FALSE ) { $r = ( isset( $this->sqltype_typeno[$typedescription] ) ) ? $this->sqltype_typeno[$typedescription] : 99; if ( $includeSpecials ) return $r; if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) { return self::C_DATATYPE_SPECIFIED; } return $r; } /** * @see QueryWriter::widenColumn */ public function widenColumn( $type, $column, $datatype ) { $table = $type; $type = $datatype; $table = $this->esc( $table ); $column = $this->esc( $column ); $newtype = $this->typeno_sqltype[$type]; $this->adapter->exec( "ALTER TABLE $table \n\t ALTER COLUMN $column TYPE $newtype " ); } /** * @see QueryWriter::addUniqueIndex */ public function addUniqueConstraint( $type, $properties ) { $tableNoQ = $this->esc( $type, TRUE ); $columns = array(); foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); $table = $this->esc( $type ); sort( $columns ); //else we get multiple indexes due to order-effects $name = "UQ_" . sha1( $table . implode( ',', $columns ) ); $sql = "ALTER TABLE {$table} ADD CONSTRAINT $name UNIQUE (" . implode( ',', $columns ) . ")"; try { $this->adapter->exec( $sql ); } catch( SQLException $e ) { return FALSE; } return TRUE; } /** * @see QueryWriter::sqlStateIn */ public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) { $stateMap = array( '42P01' => QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, '42703' => QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, '23505' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION, '55P03' => QueryWriter::C_SQLSTATE_LOCK_TIMEOUT ); return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); } /** * @see QueryWriter::addIndex */ public function addIndex( $type, $name, $property ) { $table = $this->esc( $type ); $name = preg_replace( '/\W/', '', $name ); $column = $this->esc( $property ); try { $this->adapter->exec( "CREATE INDEX {$name} ON $table ({$column}) " ); return TRUE; } catch ( SQLException $e ) { return FALSE; } } /** * @see QueryWriter::addFK */ public function addFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ) { $table = $this->esc( $type ); $targetTable = $this->esc( $targetType ); $field = $this->esc( $property ); $targetField = $this->esc( $targetProperty ); $tableNoQ = $this->esc( $type, TRUE ); $fieldNoQ = $this->esc( $property, TRUE ); if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $fieldNoQ ) ) ) return FALSE; try{ $delRule = ( $isDep ? 'CASCADE' : 'SET NULL' ); $this->adapter->exec( "ALTER TABLE {$table} ADD FOREIGN KEY ( {$field} ) REFERENCES {$targetTable} ({$targetField}) ON DELETE {$delRule} ON UPDATE {$delRule} DEFERRABLE ;" ); return TRUE; } catch ( SQLException $e ) { return FALSE; } } /** * @see QueryWriter::wipeAll */ public function wipeAll() { $this->adapter->exec( 'SET CONSTRAINTS ALL DEFERRED' ); foreach ( $this->getTables() as $t ) { $t = $this->esc( $t ); //Some plugins (PostGIS have unremovable tables/views), avoid exceptions. try { $this->adapter->exec( "DROP TABLE IF EXISTS $t CASCADE " ); }catch( \Exception $e ) {} } $this->adapter->exec( 'SET CONSTRAINTS ALL IMMEDIATE' ); } } } namespace RedBeanPHP { /** * RedBean\Exception Base. * Represents the base class for RedBeanPHP\Exceptions. * * @file RedBeanPHP/Exception.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. */ class RedException extends \Exception { } } namespace RedBeanPHP\RedException { use RedBeanPHP\RedException as RedException; /** * SQL Exception. * Represents a generic database exception independent of the underlying driver. * * @file RedBeanPHP/RedException/SQL.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @copyright * (c) copyright 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. */ class SQL extends RedException { /** * @var string */ private $sqlState; /** * @var array */ private $driverDetails = array(); /** * @return array */ public function getDriverDetails() { return $this->driverDetails; } /** * @param array $driverDetails */ public function setDriverDetails($driverDetails) { $this->driverDetails = $driverDetails; } /** * Returns an ANSI-92 compliant SQL state. * * @return string */ public function getSQLState() { return $this->sqlState; } /** * Returns the raw SQL STATE, possibly compliant with * ANSI SQL error codes - but this depends on database driver. * * @param string $sqlState SQL state error code * * @return void */ public function setSQLState( $sqlState ) { $this->sqlState = $sqlState; } /** * To String prints both code and SQL state. * * @return string */ public function __toString() { return '[' . $this->getSQLState() . '] - ' . $this->getMessage()."\n". 'trace: ' . $this->getTraceAsString(); } } } 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 accomodate 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 DBAdapter */ protected $writer; /** * @var boolean */ protected $partialBeans = FALSE; /** * 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 mixed */ public function usePartialBeans( $yesNoBeans ) { $oldValue = $this->partialBeans; $this->partialBeans = $yesNoBeans; return $oldValue; } /** * Stores a bean and its lists in one run. * * @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 SimpleModel ) ? $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 SimpleModel ) ? $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 an embedded bean. * * @param OODBBean|SimpleModel $embeddedBean the bean or model * * @return integer */ protected function prepareEmbeddedBean( $embeddedBean ) { if ( !$embeddedBean->id || $embeddedBean->getMeta( 'tainted' ) ) { $this->store( $embeddedBean ); } return $embeddedBean->id; } /** * 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. * * @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. * * @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. * * @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, removed the bean property and * stores the bean in the embedded beans array. * * @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'; $id = $this->prepareEmbeddedBean( $value ); 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. * * @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" ); } } } /** * 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: * * * array( * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' ) * 'PROPERTY' => array( POSSIBLE VALUES... ) * ); * * * 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. * * @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 $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 whos 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 = NULL ) { $masklen = 0; if ( $mask !== NULL ) $masklen = mb_strlen( $mask ); $collection = array(); $this->stash[$this->nesting] = array(); foreach ( $rows as $row ) { $meta = array(); if ( !is_null( $mask ) ) { foreach( $row as $key => $value ) { if ( strpos( $key, $mask ) === 0 ) { 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 ); if ( count( explode( '_', $type ) ) > 2 ) { throw new RedException( 'Invalid type for count.' ); } try { return (int) $this->writer->queryRecordCount( $type, array(), $addSQL, $bindings ); } catch ( SQLException $exception ) { if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), $exception->getDriverDetails() ) ) { throw $exception; } } return 0; } /** * Removes a bean from the database. * This function will remove the specified OODBBean * Bean Object from the database. * * @param OODBBean|SimpleModel $bean bean you want to remove from database * * @return void */ 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 { $this->writer->deleteRecord( $bean->getMeta( 'type' ), array( 'id' => array( $bean->id ) ), NULL ); } catch ( SQLException $exception ) { $this->handleException( $exception ); } $bean->id = 0; $this->oodb->signal( 'after_delete', $bean ); } /** * Checks whether the specified table already exists in the database. * Not part of the Object Database interface! * * @deprecated Use AQueryWriter::typeExists() instead. * * @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. * * @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; } } } } namespace RedBeanPHP\Repository { use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\RedException as RedException; use RedBeanPHP\BeanHelper as BeanHelper; use RedBeanPHP\RedException\SQL as SQLException; use RedBeanPHP\Repository as Repository; /** * Fluid Repository. * OODB manages two repositories, a fluid one that * adjust the database schema on-the-fly to accomodate 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/Fluid.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. */ class Fluid extends Repository { /** * Figures out the desired type given the cast string ID. * Given a cast ID, this method will return the associated * type (INT(10) or VARCHAR for instance). The returned type * can be processed by the Query Writer to build the specified * column for you in the database. The Cast ID is actually just * a superset of the QueryWriter types. In addition to default * Query Writer column types you can pass the following 'cast types': * 'id' and 'string'. These will map to Query Writer specific * column types (probably INT and VARCHAR). * * @param string $cast cast identifier * * @return integer */ private function getTypeFromCast( $cast ) { if ( $cast == 'string' ) { $typeno = $this->writer->scanType( 'STRING' ); } elseif ( $cast == 'id' ) { $typeno = $this->writer->getTypeForID(); } elseif ( isset( $this->writer->sqltype_typeno[$cast] ) ) { $typeno = $this->writer->sqltype_typeno[$cast]; } else { throw new RedException( 'Invalid Cast' ); } return $typeno; } /** * Orders the Query Writer to create a table if it does not exist already and * adds a note in the build report about the creation. * * @param OODBBean $bean bean to update report of * @param string $table table to check and create if not exists * * @return void */ private function createTableIfNotExists( OODBBean $bean, $table ) { //Does table exist? If not, create if ( !$this->tableExists( $this->writer->esc( $table, TRUE ) ) ) { $this->writer->createTable( $table ); $bean->setMeta( 'buildreport.flags.created', TRUE ); } } /** * Modifies the table to fit the bean data. * Given a property and a value and the bean, this method will * adjust the table structure to fit the requirements of the property and value. * This may include adding a new column or widening an existing column to hold a larger * or different kind of value. This method employs the writer to adjust the table * structure in the database. Schema updates are recorded in meta properties of the bean. * * This method will also apply indexes, unique constraints and foreign keys. * * @param OODBBean $bean bean to get cast data from and store meta in * @param string $property property to store * @param mixed $value value to store * * @return void */ private function modifySchema( OODBBean $bean, $property, $value ) { $doFKStuff = FALSE; $table = $bean->getMeta( 'type' ); $columns = $this->writer->getColumns( $table ); $columnNoQ = $this->writer->esc( $property, TRUE ); if ( !$this->oodb->isChilled( $bean->getMeta( 'type' ) ) ) { if ( $bean->getMeta( "cast.$property", -1 ) !== -1 ) { //check for explicitly specified types $cast = $bean->getMeta( "cast.$property" ); $typeno = $this->getTypeFromCast( $cast ); } else { $cast = FALSE; $typeno = $this->writer->scanType( $value, TRUE ); } if ( isset( $columns[$this->writer->esc( $property, TRUE )] ) ) { //Is this property represented in the table ? if ( !$cast ) { //rescan without taking into account special types >80 $typeno = $this->writer->scanType( $value, FALSE ); } $sqlt = $this->writer->code( $columns[$this->writer->esc( $property, TRUE )] ); if ( $typeno > $sqlt ) { //no, we have to widen the database column type $this->writer->widenColumn( $table, $property, $typeno ); $bean->setMeta( 'buildreport.flags.widen', TRUE ); $doFKStuff = TRUE; } } else { $this->writer->addColumn( $table, $property, $typeno ); $bean->setMeta( 'buildreport.flags.addcolumn', TRUE ); $doFKStuff = TRUE; } if ($doFKStuff) { if (strrpos($columnNoQ, '_id')===(strlen($columnNoQ)-3)) { $destinationColumnNoQ = substr($columnNoQ, 0, strlen($columnNoQ)-3); $indexName = "index_foreignkey_{$table}_{$destinationColumnNoQ}"; $this->writer->addIndex($table, $indexName, $columnNoQ); $typeof = $bean->getMeta("sys.typeof.{$destinationColumnNoQ}", $destinationColumnNoQ); $isLink = $bean->getMeta( 'sys.buildcommand.unique', FALSE ); //Make FK CASCADING if part of exclusive list (dependson=typeof) or if link bean $isDep = ( $bean->moveMeta( 'sys.buildcommand.fkdependson' ) === $typeof || is_array( $isLink ) ); $result = $this->writer->addFK( $table, $typeof, $columnNoQ, 'id', $isDep ); //If this is a link bean and all unique columns have been added already, then apply unique constraint if ( is_array( $isLink ) && !count( array_diff( $isLink, array_keys( $this->writer->getColumns( $table ) ) ) ) ) { $this->writer->addUniqueConstraint( $table, $bean->moveMeta('sys.buildcommand.unique') ); $bean->setMeta("sys.typeof.{$destinationColumnNoQ}", NULL); } } } } } /** * Part of the store() functionality. * Handles all new additions after the bean has been saved. * Stores addition bean in own-list, extracts the id and * adds a foreign key. Also adds a constraint in case the type is * in the dependent list. * * Note that this method raises a custom exception if the bean * is not an instance of OODBBean. Therefore it does not use * a type hint. This allows the user to take action in case * invalid objects are passed in the list. * * @param OODBBean $bean bean to process * @param array $ownAdditions list of addition beans in own-list * * @return void */ protected function processAdditions( $bean, $ownAdditions ) { $beanType = $bean->getMeta( 'type' ); foreach ( $ownAdditions as $addition ) { if ( $addition instanceof OODBBean ) { $myFieldLink = $beanType . '_id'; $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) ); if ( $alias ) $myFieldLink = $alias . '_id'; $addition->$myFieldLink = $bean->id; $addition->setMeta( 'cast.' . $myFieldLink, 'id' ); if ($alias) { $addition->setMeta( "sys.typeof.{$alias}", $beanType ); } else { $addition->setMeta( "sys.typeof.{$beanType}", $beanType ); } $this->store( $addition ); } else { throw new RedException( 'Array may only contain OODBBeans' ); } } } /** * Stores a cleaned bean; i.e. only scalar values. This is the core of the store() * method. When all lists and embedded beans (parent objects) have been processed and * removed from the original bean the bean is passed to this method to be stored * in the database. * * @param OODBBean $bean the clean bean * * @return void */ protected function storeBean( OODBBean $bean ) { if ( $bean->getMeta( 'changed' ) ) { $this->check( $bean ); $table = $bean->getMeta( 'type' ); $this->createTableIfNotExists( $bean, $table ); $updateValues = array(); $partial = ( $this->partialBeans === TRUE || ( is_array( $this->partialBeans ) && in_array( $table, $this->partialBeans ) ) ); if ( $partial ) { $mask = $bean->getMeta( 'changelist' ); $bean->setMeta( 'changelist', array() ); } foreach ( $bean as $property => $value ) { if ( $partial && !in_array( $property, $mask ) ) continue; if ( $property !== 'id' ) { $this->modifySchema( $bean, $property, $value ); } if ( $property !== 'id' ) { $updateValues[] = array( 'property' => $property, 'value' => $value ); } } $bean->id = $this->writer->updateRecord( $table, $updateValues, $bean->id ); $bean->setMeta( 'changed', FALSE ); } $bean->setMeta( 'tainted', FALSE ); } /** * Handles exceptions. Suppresses exceptions caused by missing structures. * * @param Exception $exception exception * * @return void */ protected function handleException( \Exception $exception ) { if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), $exception->getDriverDetails() ) ) { throw $exception; } } /** * 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. * * @param string $type type of bean you want to dispense * @param string $number number of beans you would like to get * @param boolean $alwaysReturnArray if TRUE always returns the result as an array * * @return 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; } /** * Loads a bean from the object database. * It searches for a OODBBean Bean Object in the * database. It does not matter how this bean has been stored. * RedBean uses the primary key ID $id and the string $type * to find the bean. The $type specifies what kind of bean you * are looking for; this is the same type as used with the * dispense() function. If RedBean finds the bean it will return * the OODB Bean object; if it cannot find the bean * RedBean will return a new bean of type $type and with * primary key ID 0. In the latter case it acts basically the * same as dispense(). * * Important note: * If the bean cannot be found in the database a new bean of * the specified type will be generated and returned. * * @param string $type type of bean you want to load * @param integer $id ID of the bean you want to load * * @return OODBBean */ public function load( $type, $id ) { $rows = array(); $bean = $this->dispense( $type ); if ( isset( $this->stash[$this->nesting][$id] ) ) { $row = $this->stash[$this->nesting][$id]; } else { try { $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) ); } catch ( SQLException $exception ) { if ( $this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ), $exception->getDriverDetails() ) ) { $rows = array(); } else { throw $exception; } } if ( !count( $rows ) ) { return $bean; } $row = array_pop( $rows ); } $bean->importRow( $row ); $this->nesting++; $this->oodb->signal( 'open', $bean ); $this->nesting--; return $bean->setMeta( 'tainted', FALSE ); } } } namespace RedBeanPHP\Repository { use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\RedException as RedException; use RedBeanPHP\BeanHelper as BeanHelper; use RedBeanPHP\RedException\SQL as SQLException; use RedBeanPHP\Repository as Repository; /** * Frozen Repository. * OODB manages two repositories, a fluid one that * adjust the database schema on-the-fly to accomodate 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/Frozen.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. */ class Frozen extends Repository { /** * Handles exceptions. * In fluid mode, this suppresses exceptions caused by missing structures. * However the implementation in frozen mode is rather the opposite, it * will just re-throw every exception. * * @param \Exception $exception exception to handle * * @return void */ protected function handleException( \Exception $exception ) { throw $exception; } /** * Stores a cleaned bean; i.e. only scalar values. This is the core of the store() * method. When all lists and embedded beans (parent objects) have been processed and * removed from the original bean the bean is passed to this method to be stored * in the database. * * @param OODBBean $bean the clean bean * * @return void */ protected function storeBean( OODBBean $bean ) { if ( $bean->getMeta( 'changed' ) ) { list( $properties, $table ) = $bean->getPropertiesAndType(); $id = $properties['id']; unset($properties['id']); $updateValues = array(); $k1 = 'property'; $k2 = 'value'; $partial = ( $this->partialBeans === TRUE || ( is_array( $this->partialBeans ) && in_array( $table, $this->partialBeans ) ) ); if ( $partial ) { $mask = $bean->getMeta( 'changelist' ); $bean->setMeta( 'changelist', array() ); } foreach( $properties as $key => $value ) { if ( $partial && !in_array( $key, $mask ) ) continue; $updateValues[] = array( $k1 => $key, $k2 => $value ); } $bean->id = $this->writer->updateRecord( $table, $updateValues, $id ); $bean->setMeta( 'changed', FALSE ); } $bean->setMeta( 'tainted', FALSE ); } /** * Part of the store() functionality. * Handles all new additions after the bean has been saved. * Stores addition bean in own-list, extracts the id and * adds a foreign key. Also adds a constraint in case the type is * in the dependent list. * * Note that this method raises a custom exception if the bean * is not an instance of OODBBean. Therefore it does not use * a type hint. This allows the user to take action in case * invalid objects are passed in the list. * * @param OODBBean $bean bean to process * @param array $ownAdditions list of addition beans in own-list * * @return void * @throws RedException */ protected function processAdditions( $bean, $ownAdditions ) { $beanType = $bean->getMeta( 'type' ); $cachedIndex = array(); foreach ( $ownAdditions as $addition ) { if ( $addition instanceof OODBBean ) { $myFieldLink = $beanType . '_id'; $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) ); if ( $alias ) $myFieldLink = $alias . '_id'; $addition->$myFieldLink = $bean->id; $addition->setMeta( 'cast.' . $myFieldLink, 'id' ); $this->store( $addition ); } else { throw new RedException( 'Array may only contain OODBBeans' ); } } } /** * 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. * * @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 */ 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++ ) { /** @var \RedBeanPHP\OODBBean $bean */ $bean = new $OODBBEAN; $bean->initializeForDispense( $type, $this->oodb->getBeanHelper() ); $this->oodb->signal( 'dispense', $bean ); $beans[] = $bean; } return ( count( $beans ) === 1 && !$alwaysReturnArray ) ? array_pop( $beans ) : $beans; } /** * Loads a bean from the object database. * It searches for a OODBBean Bean Object in the * database. It does not matter how this bean has been stored. * RedBean uses the primary key ID $id and the string $type * to find the bean. The $type specifies what kind of bean you * are looking for; this is the same type as used with the * dispense() function. If RedBean finds the bean it will return * the OODB Bean object; if it cannot find the bean * RedBean will return a new bean of type $type and with * primary key ID 0. In the latter case it acts basically the * same as dispense(). * * Important note: * If the bean cannot be found in the database a new bean of * the specified type will be generated and returned. * * @param string $type type of bean you want to load * @param integer $id ID of the bean you want to load * * @return OODBBean * @throws SQLException */ public function load( $type, $id ) { $rows = array(); $bean = $this->dispense( $type ); if ( isset( $this->stash[$this->nesting][$id] ) ) { $row = $this->stash[$this->nesting][$id]; } else { $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) ); if ( !count( $rows ) ) { return $bean; } $row = array_pop( $rows ); } $bean->importRow( $row ); $this->nesting++; $this->oodb->signal( 'open', $bean ); $this->nesting--; return $bean->setMeta( 'tainted', FALSE ); } } } namespace RedBeanPHP { use RedBeanPHP\Adapter\DBAdapter as DBAdapter; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\BeanHelper as BeanHelper; use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; use RedBeanPHP\Repository as Repository; use RedBeanPHP\Repository\Fluid as FluidRepo; use RedBeanPHP\Repository\Frozen as FrozenRepo; /** * RedBean Object Oriented DataBase. * * The RedBean OODB Class is the main class of RedBeanPHP. * It takes OODBBean objects and stores them to and loads them from the * database as well as providing other CRUD functions. This class acts as a * object database. * * @file RedBeanPHP/OODB.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. */ class OODB extends Observable { /** * @var array */ private static $sqlFilters = array(); /** * @var array */ protected $chillList = array(); /** * @var array */ protected $stash = NULL; /* * @var integer */ protected $nesting = 0; /** * @var DBAdapter */ protected $writer; /** * @var boolean */ protected $isFrozen = FALSE; /** * @var FacadeBeanHelper */ protected $beanhelper = NULL; /** * @var AssociationManager */ protected $assocManager = NULL; /** * @var Repository */ protected $repository = NULL; /** * @var FrozenRepo */ protected $frozenRepository = NULL; /** * @var FluidRepo */ protected $fluidRepository = NULL; /** * @var boolean */ protected static $autoClearHistoryAfterStore = FALSE; /** * If set to TRUE, this method will call clearHistory every time * the bean gets stored. * * @param boolean $autoClear auto clear option * * @return void */ public static function autoClearHistoryAfterStore( $autoClear = TRUE ) { self::$autoClearHistoryAfterStore = (boolean) $autoClear; } /** * Unboxes a bean from a FUSE model if needed and checks whether the bean is * an instance of OODBBean. * * @param OODBBean $bean bean you wish to unbox * * @return OODBBean */ protected function unboxIfNeeded( $bean ) { if ( $bean instanceof SimpleModel ) { $bean = $bean->unbox(); } if ( !( $bean instanceof OODBBean ) ) { throw new RedException( 'OODB Store requires a bean, got: ' . gettype( $bean ) ); } return $bean; } /** * Constructor, requires a query writer. * * @param QueryWriter $writer writer * @param array|boolean $frozen mode of operation: TRUE (frozen), FALSE (default, fluid) or ARRAY (chilled) */ public function __construct( QueryWriter $writer, $frozen = FALSE ) { if ( $writer instanceof QueryWriter ) { $this->writer = $writer; } $this->freeze( $frozen ); } /** * Toggles fluid or frozen mode. In fluid mode the database * structure is adjusted to accomodate your objects. In frozen mode * this is not the case. * * You can also pass an array containing a selection of frozen types. * Let's call this chilly mode, it's just like fluid mode except that * certain types (i.e. tables) aren't touched. * * @param boolean|array $toggle TRUE if you want to use OODB instance in frozen mode * * @return void */ public function freeze( $toggle ) { if ( is_array( $toggle ) ) { $this->chillList = $toggle; $this->isFrozen = FALSE; } else { $this->isFrozen = (boolean) $toggle; } if ( $this->isFrozen ) { if ( !$this->frozenRepository ) { $this->frozenRepository = new FrozenRepo( $this, $this->writer ); } $this->repository = $this->frozenRepository; } else { if ( !$this->fluidRepository ) { $this->fluidRepository = new FluidRepo( $this, $this->writer ); } $this->repository = $this->fluidRepository; } if ( count( self::$sqlFilters ) ) { AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) ); } } /** * Returns the current mode of operation of RedBean. * In fluid mode the database * structure is adjusted to accomodate your objects. * In frozen mode * this is not the case. * * @return boolean */ public function isFrozen() { return (bool) $this->isFrozen; } /** * Determines whether a type is in the chill list. * If a type is 'chilled' it's frozen, so its schema cannot be * changed anymore. However other bean types may still be modified. * This method is a convenience method for other objects to check if * the schema of a certain type is locked for modification. * * @param string $type the type you wish to check * * @return boolean */ public function isChilled( $type ) { return (boolean) ( in_array( $type, $this->chillList ) ); } /** * 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. * * @param string $type type of bean you want to dispense * @param string $number number of beans you would like to get * @param boolean $alwaysReturnArray if TRUE always returns the result as an array * * @return OODBBean */ public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE ) { if ( $number < 1 ) { if ( $alwaysReturnArray ) return array(); return NULL; } return $this->repository->dispense( $type, $number, $alwaysReturnArray ); } /** * Sets bean helper to be given to beans. * Bean helpers assist beans in getting a reference to a toolbox. * * @param BeanHelper $beanhelper helper * * @return void */ public function setBeanHelper( BeanHelper $beanhelper ) { $this->beanhelper = $beanhelper; } /** * Returns the current bean helper. * Bean helpers assist beans in getting a reference to a toolbox. * * @return BeanHelper */ public function getBeanHelper() { return $this->beanhelper; } /** * 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. * * @param OODBBean $bean the bean that needs to be checked * * @return void */ public function check( OODBBean $bean ) { $this->repository->check( $bean ); } /** * 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: * * * array( * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' ) * 'PROPERTY' => array( POSSIBLE VALUES... ) * ); * * * 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 a list of values to bind to query parameters * * @return array */ public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() ) { return $this->repository->find( $type, $conditions, $sql, $bindings ); } /** * Same as find() but returns a BeanCollection. * * @param string $type type of beans you are looking for * @param string $sql SQL to be used in query * @param array $bindings a list of values to bind to query parameters * * @return array */ public function findCollection( $type, $sql = NULL, $bindings = array() ) { return $this->repository->findCollection( $type, $sql, $bindings ); } /** * Checks whether the specified table already exists in the database. * Not part of the Object Database interface! * * @deprecated Use AQueryWriter::typeExists() instead. * * @param string $table table name * * @return boolean */ public function tableExists( $table ) { return $this->repository->tableExists( $table ); } /** * 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 $bean bean to store * * @return integer|string */ public function store( $bean ) { $bean = $this->unboxIfNeeded( $bean ); $id = $this->repository->store( $bean ); if ( self::$autoClearHistoryAfterStore ) { $bean->clearHistory(); } return $id; } /** * Loads a bean from the object database. * It searches for a OODBBean Bean Object in the * database. It does not matter how this bean has been stored. * RedBean uses the primary key ID $id and the string $type * to find the bean. The $type specifies what kind of bean you * are looking for; this is the same type as used with the * dispense() function. If RedBean finds the bean it will return * the OODB Bean object; if it cannot find the bean * RedBean will return a new bean of type $type and with * primary key ID 0. In the latter case it acts basically the * same as dispense(). * * Important note: * If the bean cannot be found in the database a new bean of * the specified type will be generated and returned. * * @param string $type type of bean you want to load * @param integer $id ID of the bean you want to load * * @return OODBBean */ public function load( $type, $id ) { return $this->repository->load( $type, $id ); } /** * Removes a bean from the database. * This function will remove the specified OODBBean * Bean Object from the database. * * @param OODBBean|SimpleModel $bean bean you want to remove from database * * @return void */ public function trash( $bean ) { $bean = $this->unboxIfNeeded( $bean ); return $this->repository->trash( $bean ); } /** * 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 ) { return $this->repository->batch( $type, $ids ); } /** * 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. * * @param string $type type of beans you would like to have * @param array $rows rows from the database result * @param string $mask mask to apply for meta data * * @return array */ public function convertToBeans( $type, $rows, $mask = NULL ) { return $this->repository->convertToBeans( $type, $rows, $mask ); } /** * 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() ) { return $this->repository->count( $type, $addSQL, $bindings ); } /** * Trash all beans of a given type. Wipes an entire type of bean. * * @param string $type type of bean you wish to delete all instances of * * @return boolean */ public function wipe( $type ) { return $this->repository->wipe( $type ); } /** * Returns an Association Manager for use with OODB. * A simple getter function to obtain a reference to the association manager used for * storage and more. * * @return AssociationManager */ public function getAssociationManager() { if ( !isset( $this->assocManager ) ) { throw new RedException( 'No association manager available.' ); } return $this->assocManager; } /** * Sets the association manager instance to be used by this OODB. * A simple setter function to set the association manager to be used for storage and * more. * * @param AssociationManager $assocManager sets the association manager to be used * * @return void */ public function setAssociationManager( AssociationManager $assocManager ) { $this->assocManager = $assocManager; } /** * Returns the currently used repository instance. * For testing purposes only. * * @return Repository */ public function getCurrentRepository() { return $this->repository; } /** * Binds an SQL function to a column. * This method can be used to setup a decode/encode scheme or * perform UUID insertion. This method is especially useful for handling * MySQL spatial columns, because they need to be processed first using * the asText/GeomFromText functions. * * @param string $mode mode to set function for, i.e. read or write * @param string $field field (table.column) to bind SQL function to * @param string $function SQL function to bind to field * * @return void */ public function bindFunc( $mode, $field, $function ) { list( $type, $property ) = explode( '.', $field ); $mode = ($mode === 'write') ? QueryWriter::C_SQLFILTER_WRITE : QueryWriter::C_SQLFILTER_READ; if ( !isset( self::$sqlFilters[$mode] ) ) self::$sqlFilters[$mode] = array(); if ( !isset( self::$sqlFilters[$mode][$type] ) ) self::$sqlFilters[$mode][$type] = array(); if ( is_null( $function ) ) { unset( self::$sqlFilters[$mode][$type][$property] ); } else { if ($mode === QueryWriter::C_SQLFILTER_WRITE) { self::$sqlFilters[$mode][$type][$property] = $function.'(?)'; } else { self::$sqlFilters[$mode][$type][$property] = $function."($field)"; } } AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) ); } } } namespace RedBeanPHP { use RedBeanPHP\OODB as OODB; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\Adapter\DBAdapter as DBAdapter; use RedBeanPHP\Adapter as Adapter; /** * ToolBox. * * The toolbox is an integral part of RedBeanPHP providing the basic * architectural building blocks to manager objects, helpers and additional tools * like plugins. A toolbox contains the three core components of RedBeanPHP: * the adapter, the query writer and the core functionality of RedBeanPHP in * OODB. * * @file RedBeanPHP/ToolBox.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. */ class ToolBox { /** * @var OODB */ protected $oodb; /** * @var QueryWriter */ protected $writer; /** * @var DBAdapter */ protected $adapter; /** * Constructor. * The toolbox is an integral part of RedBeanPHP providing the basic * architectural building blocks to manager objects, helpers and additional tools * like plugins. A toolbox contains the three core components of RedBeanPHP: * the adapter, the query writer and the core functionality of RedBeanPHP in * OODB. * * @param OODB $oodb Object Database, OODB * @param DBAdapter $adapter Database Adapter * @param QueryWriter $writer Query Writer */ public function __construct( OODB $oodb, Adapter $adapter, QueryWriter $writer ) { $this->oodb = $oodb; $this->adapter = $adapter; $this->writer = $writer; return $this; } /** * Returns the query writer in this toolbox. * The Query Writer is responsible for building the queries for a * specific database and executing them through the adapter. * * @return QueryWriter */ public function getWriter() { return $this->writer; } /** * Returns the OODB instance in this toolbox. * OODB is responsible for creating, storing, retrieving and deleting * single beans. Other components rely * on OODB for their basic functionality. * * @return OODB */ public function getRedBean() { return $this->oodb; } /** * Returns the database adapter in this toolbox. * The adapter is responsible for executing the query and binding the values. * The adapter also takes care of transaction handling. * * @return DBAdapter */ public function getDatabaseAdapter() { return $this->adapter; } } } namespace RedBeanPHP { /** * RedBeanPHP Finder. * Service class to find beans. For the most part this class * offers user friendly utility methods for interacting with the * OODB::find() method, which is rather complex. This class can be * used to find beans using plain old SQL queries. * * @file RedBeanPHP/Finder.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. */ class Finder { /** * @var ToolBox */ protected $toolbox; /** * @var OODB */ protected $redbean; /** * Constructor. * The Finder requires a toolbox. * * @param ToolBox $toolbox */ public function __construct( ToolBox $toolbox ) { $this->toolbox = $toolbox; $this->redbean = $toolbox->getRedBean(); } /** * Finds a bean using a type and a where clause (SQL). * As with most Query tools in RedBean you can provide values to * be inserted in the SQL statement by populating the value * array parameter; you can either use the question mark notation * or the slot-notation (:keyname). * * @param string $type type the type of bean you are looking for * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return array */ public function find( $type, $sql = NULL, $bindings = array() ) { if ( !is_array( $bindings ) ) { throw new RedException( 'Expected array, ' . gettype( $bindings ) . ' given.' ); } return $this->redbean->find( $type, array(), $sql, $bindings ); } /** * Like find() but also exports the beans as an array. * This method will perform a find-operation. For every bean * in the result collection this method will call the export() method. * This method returns an array containing the array representations * of every bean in the result set. * * @see Finder::find * * @param string $type type the type of bean you are looking for * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return array */ public function findAndExport( $type, $sql = NULL, $bindings = array() ) { $arr = array(); foreach ( $this->find( $type, $sql, $bindings ) as $key => $item ) { $arr[] = $item->export(); } return $arr; } /** * Like find() but returns just one bean instead of an array of beans. * This method will return only the first bean of the array. * If no beans are found, this method will return NULL. * * @see Finder::find * * @param string $type type the type of bean you are looking for * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return OODBBean */ public function findOne( $type, $sql = NULL, $bindings = array() ) { $sql = $this->toolbox->getWriter()->glueLimitOne( $sql ); $items = $this->find( $type, $sql, $bindings ); if ( empty($items) ) { return NULL; } return reset( $items ); } /** * Like find() but returns the last bean of the result array. * Opposite of Finder::findLast(). * If no beans are found, this method will return NULL. * * @see Finder::find * * @param string $type the type of bean you are looking for * @param string $sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return OODBBean */ public function findLast( $type, $sql = NULL, $bindings = array() ) { $items = $this->find( $type, $sql, $bindings ); if ( empty($items) ) { return NULL; } return end( $items ); } /** * Tries to find beans of a certain type, * if no beans are found, it dispenses a bean of that type. * Note that this function always returns an array. * * @see Finder::find * * @param string $type the type of bean you are looking for * @param string $sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return array */ public function findOrDispense( $type, $sql = NULL, $bindings = array() ) { $foundBeans = $this->find( $type, $sql, $bindings ); if ( empty( $foundBeans ) ) { return array( $this->redbean->dispense( $type ) ); } else { return $foundBeans; } } /** * Finds a BeanCollection using the repository. * A bean collection can be used to retrieve one bean at a time using * cursors - this is useful for processing large datasets. A bean collection * will not load all beans into memory all at once, just one at a time. * * @param string $type the type of bean you are looking for * @param string $sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return BeanCollection */ public function findCollection( $type, $sql, $bindings = array() ) { return $this->redbean->findCollection( $type, $sql, $bindings ); } /** * Finds or creates a bean. * Tries to find a bean with certain properties specified in the second * parameter ($like). If the bean is found, it will be returned. * If multiple beans are found, only the first will be returned. * If no beans match the criteria, a new bean will be dispensed, * the criteria will be imported as properties and this new bean * will be stored and returned. * * Format of criteria set: property => value * The criteria set also supports OR-conditions: property => array( value1, orValue2 ) * * @param string $type type of bean to search for * @param array $like criteria set describing bean to search for * * @return OODBBean */ public function findOrCreate( $type, $like = array() ) { $beans = $this->findLike( $type, $like ); if ( count( $beans ) ) { $bean = reset( $beans ); return $bean; } $bean = $this->redbean->dispense( $type ); $bean->import( $like ); $this->redbean->store( $bean ); return $bean; } /** * Finds beans by its type and a certain criteria set. * * Format of criteria set: property => value * The criteria set also supports OR-conditions: property => array( value1, orValue2 ) * * If the additional SQL is a condition, this condition will be glued to the rest * of the query using an AND operator. Note that this is as far as this method * can go, there is no way to glue additional SQL using an OR-condition. * This method provides access to an underlying mechanism in the RedBeanPHP architecture * to find beans using criteria sets. However, please do not use this method * for complex queries, use plain SQL instead ( the regular find method ) as it is * more suitable for the job. This method is * meant for basic search-by-example operations. * * @param string $type type of bean to search for * @param array $conditions criteria set describing the bean to search for * @param string $sql additional SQL (for sorting) * * @return array */ public function findLike( $type, $conditions = array(), $sql = '' ) { if ( count( $conditions ) > 0 ) { foreach( $conditions as $key => $condition ) { if ( !count( $condition ) ) unset( $conditions[$key] ); } } return $this->redbean->find( $type, $conditions, $sql ); } /** * Returns a hashmap with bean arrays keyed by type using an SQL * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review' * this method will return movie and review beans. * * Example: * * * $stuff = $finder->findMulti('movie,review', ' * SELECT movie.*, review.* FROM movie * LEFT JOIN review ON review.movie_id = movie.id'); * * * After this operation, $stuff will contain an entry 'movie' containing all * movies and an entry named 'review' containing all reviews (all beans). * You can also pass bindings. * * If you want to re-map your beans, so you can use $movie->ownReviewList without * having RedBeanPHP executing an SQL query you can use the fourth parameter to * define a selection of remapping closures. * * The remapping argument (optional) should contain an array of arrays. * Each array in the remapping array should contain the following entries: * * * array( * 'a' => TYPE A * 'b' => TYPE B * 'matcher' => MATCHING FUNCTION ACCEPTING A, B and ALL BEANS * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS * ) * * * Using this mechanism you can build your own 'preloader' with tiny function * snippets (and those can be re-used and shared online of course). * * Example: * * * array( * 'a' => 'movie' //define A as movie * 'b' => 'review' //define B as review * 'matcher' => function( $a, $b ) { * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id * } * 'do' => function( $a, $b ) { * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'. * } * ) * * * The Query Template parameter is optional as well but can be used to * set a different SQL template (sprintf-style) for processing the original query. * * @note the SQL query provided IS NOT THE ONE used internally by this function, * this function will pre-process the query to get all the data required to find the beans. * * @note if you use the 'book.*' notation make SURE you're * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because * it's actually an SQL-like template SLOT, not real SQL. * * @note instead of an SQL query you can pass a result array as well. * * @param string|array $types a list of types (either array or comma separated string) * @param string|array $sql an SQL query or an array of prefetched records * @param array $bindings optional, bindings for SQL query * @param array $remappings optional, an array of remapping arrays * @param string $queryTemplate optional, query template * * @return array */ public function findMulti( $types, $sql, $bindings = array(), $remappings = array(), $queryTemplate = ' %s.%s AS %s__%s' ) { if ( !is_array( $types ) ) $types = explode( ',', $types ); if ( !is_array( $sql ) ) { $writer = $this->toolbox->getWriter(); $adapter = $this->toolbox->getDatabaseAdapter(); //Repair the query, replace book.* with book.id AS book_id etc.. foreach( $types as $type ) { $pattern = " {$type}.*"; if ( strpos( $sql, $pattern ) !== FALSE ) { $newSelectorArray = array(); $columns = $writer->getColumns( $type ); foreach( $columns as $column => $definition ) { $newSelectorArray[] = sprintf( $queryTemplate, $type, $column, $type, $column ); } $newSelector = implode( ',', $newSelectorArray ); $sql = str_replace( $pattern, $newSelector, $sql ); } } $rows = $adapter->get( $sql, $bindings ); } else { $rows = $sql; } //Gather the bean data from the query results using the prefix $wannaBeans = array(); foreach( $types as $type ) { $wannaBeans[$type] = array(); $prefix = "{$type}__"; foreach( $rows as $rowkey=>$row ) { $wannaBean = array(); foreach( $row as $cell => $value ) { if ( strpos( $cell, $prefix ) === 0 ) { $property = substr( $cell, strlen( $prefix ) ); unset( $rows[$rowkey][$cell] ); $wannaBean[$property] = $value; } } if ( !isset( $wannaBean['id'] ) ) continue; if ( is_null( $wannaBean['id'] ) ) continue; $wannaBeans[$type][$wannaBean['id']] = $wannaBean; } } //Turn the rows into beans $beans = array(); foreach( $wannaBeans as $type => $wannabees ) { $beans[$type] = $this->redbean->convertToBeans( $type, $wannabees ); } //Apply additional re-mappings foreach($remappings as $remapping) { $a = $remapping['a']; $b = $remapping['b']; $matcher = $remapping['matcher']; $do = $remapping['do']; foreach( $beans[$a] as $bean ) { foreach( $beans[$b] as $putBean ) { if ( $matcher( $bean, $putBean, $beans ) ) $do( $bean, $putBean, $beans, $remapping ); } } } return $beans; } } } namespace RedBeanPHP { use RedBeanPHP\Adapter\DBAdapter as DBAdapter; use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\RedException as RedException; use RedBeanPHP\RedException\SQL as SQLException; /** * Association Manager. * Manages simple bean associations. * * @file RedBeanPHP/AssociationManager.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @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. */ class AssociationManager extends Observable { /** * @var OODB */ protected $oodb; /** * @var DBAdapter */ protected $adapter; /** * @var QueryWriter */ protected $writer; /** * Handles exceptions. Suppresses exceptions caused by missing structures. * * @param \Exception $exception exception to handle * * @return void */ private function handleException( \Exception $exception ) { if ( $this->oodb->isFrozen() || !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), $exception->getDriverDetails() ) ) { throw $exception; } } /** * Internal method. * Returns the many-to-many related rows of table $type for bean $bean using additional SQL in $sql and * $bindings bindings. If $getLinks is TRUE, link rows are returned instead. * * @param OODBBean $bean reference bean instance * @param string $type target bean type * @param string $sql additional SQL snippet * @param array $bindings bindings for query * * @return array */ private function relatedRows( $bean, $type, $sql = '', $bindings = array() ) { $ids = array( $bean->id ); $sourceType = $bean->getMeta( 'type' ); try { return $this->writer->queryRecordRelated( $sourceType, $type, $ids, $sql, $bindings ); } catch ( SQLException $exception ) { $this->handleException( $exception ); return array(); } } /** * Associates a pair of beans. This method associates two beans, no matter * what types. Accepts a base bean that contains data for the linking record. * This method is used by associate. This method also accepts a base bean to be used * as the template for the link record in the database. * * @param OODBBean $bean1 first bean * @param OODBBean $bean2 second bean * @param OODBBean $bean base bean (association record) * * @return mixed */ protected function associateBeans( OODBBean $bean1, OODBBean $bean2, OODBBean $bean ) { $type = $bean->getMeta( 'type' ); $property1 = $bean1->getMeta( 'type' ) . '_id'; $property2 = $bean2->getMeta( 'type' ) . '_id'; if ( $property1 == $property2 ) { $property2 = $bean2->getMeta( 'type' ) . '2_id'; } $this->oodb->store( $bean1 ); $this->oodb->store( $bean2 ); $bean->setMeta( "cast.$property1", "id" ); $bean->setMeta( "cast.$property2", "id" ); $bean->setMeta( 'sys.buildcommand.unique', array( $property1, $property2 ) ); $bean->$property1 = $bean1->id; $bean->$property2 = $bean2->id; $results = array(); try { $id = $this->oodb->store( $bean ); $results[] = $id; } catch ( SQLException $exception ) { if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION ), $exception->getDriverDetails() ) ) { throw $exception; } } return $results; } /** * Constructor * * @param ToolBox $tools toolbox */ public function __construct( ToolBox $tools ) { $this->oodb = $tools->getRedBean(); $this->adapter = $tools->getDatabaseAdapter(); $this->writer = $tools->getWriter(); $this->toolbox = $tools; } /** * Creates a table name based on a types array. * Manages the get the correct name for the linking table for the * types provided. * * @param array $types 2 types as strings * * @return string */ public function getTable( $types ) { return $this->writer->getAssocTable( $types ); } /** * Associates two beans in a many-to-many relation. * This method will associate two beans and store the connection between the * two in a link table. Instead of two single beans this method also accepts * two sets of beans. Returns the ID or the IDs of the linking beans. * * @param OODBBean|array $beans1 one or more beans to form the association * @param OODBBean|array $beans2 one or more beans to form the association * * @return array */ public function associate( $beans1, $beans2 ) { if ( !is_array( $beans1 ) ) { $beans1 = array( $beans1 ); } if ( !is_array( $beans2 ) ) { $beans2 = array( $beans2 ); } $results = array(); foreach ( $beans1 as $bean1 ) { foreach ( $beans2 as $bean2 ) { $table = $this->getTable( array( $bean1->getMeta( 'type' ), $bean2->getMeta( 'type' ) ) ); $bean = $this->oodb->dispense( $table ); $results[] = $this->associateBeans( $bean1, $bean2, $bean ); } } return ( count( $results ) > 1 ) ? $results : reset( $results ); } /** * Counts the number of related beans in an N-M relation. * This method returns the number of beans of type $type associated * with reference bean(s) $bean. The query can be tuned using an * SQL snippet for additional filtering. * * @param OODBBean|array $bean a bean object or an array of beans * @param string $type type of bean you're interested in * @param string $sql SQL snippet (optional) * @param array $bindings bindings for your SQL string * * @return integer */ public function relatedCount( $bean, $type, $sql = NULL, $bindings = array() ) { if ( !( $bean instanceof OODBBean ) ) { throw new RedException( 'Expected array or OODBBean but got:' . gettype( $bean ) ); } if ( !$bean->id ) { return 0; } $beanType = $bean->getMeta( 'type' ); try { return $this->writer->queryRecordCountRelated( $beanType, $type, $bean->id, $sql, $bindings ); } catch ( SQLException $exception ) { $this->handleException( $exception ); return 0; } } /** * Breaks the association between two beans. This method unassociates two beans. If the * method succeeds the beans will no longer form an association. In the database * this means that the association record will be removed. This method uses the * OODB trash() method to remove the association links, thus giving FUSE models the * opportunity to hook-in additional business logic. If the $fast parameter is * set to boolean TRUE this method will remove the beans without their consent, * bypassing FUSE. This can be used to improve performance. * * @param OODBBean $beans1 first bean in target association * @param OODBBean $beans2 second bean in target association * @param boolean $fast if TRUE, removes the entries by query without FUSE * * @return void */ public function unassociate( $beans1, $beans2, $fast = NULL ) { $beans1 = ( !is_array( $beans1 ) ) ? array( $beans1 ) : $beans1; $beans2 = ( !is_array( $beans2 ) ) ? array( $beans2 ) : $beans2; foreach ( $beans1 as $bean1 ) { foreach ( $beans2 as $bean2 ) { try { $this->oodb->store( $bean1 ); $this->oodb->store( $bean2 ); $type1 = $bean1->getMeta( 'type' ); $type2 = $bean2->getMeta( 'type' ); $row = $this->writer->queryRecordLink( $type1, $type2, $bean1->id, $bean2->id ); $linkType = $this->getTable( array( $type1, $type2 ) ); if ( $fast ) { $this->writer->deleteRecord( $linkType, array( 'id' => $row['id'] ) ); return; } $beans = $this->oodb->convertToBeans( $linkType, array( $row ) ); if ( count( $beans ) > 0 ) { $bean = reset( $beans ); $this->oodb->trash( $bean ); } } catch ( SQLException $exception ) { $this->handleException( $exception ); } } } } /** * Removes all relations for a bean. This method breaks every connection between * a certain bean $bean and every other bean of type $type. Warning: this method * is really fast because it uses a direct SQL query however it does not inform the * models about this. If you want to notify FUSE models about deletion use a foreach-loop * with unassociate() instead. (that might be slower though) * * @param OODBBean $bean reference bean * @param string $type type of beans that need to be unassociated * * @return void */ public function clearRelations( OODBBean $bean, $type ) { $this->oodb->store( $bean ); try { $this->writer->deleteRelations( $bean->getMeta( 'type' ), $type, $bean->id ); } catch ( SQLException $exception ) { $this->handleException( $exception ); } } /** * Returns all the beans associated with $bean. * This method will return an array containing all the beans that have * been associated once with the associate() function and are still * associated with the bean specified. The type parameter indicates the * type of beans you are looking for. You can also pass some extra SQL and * values for that SQL to filter your results after fetching the * related beans. * * Don't try to make use of subqueries, a subquery using IN() seems to * be slower than two queries! * * Since 3.2, you can now also pass an array of beans instead just one * bean as the first parameter. * * @param OODBBean|array $bean the bean you have * @param string $type the type of beans you want * @param string $sql SQL snippet for extra filtering * @param array $bindings values to be inserted in SQL slots * * @return array */ public function related( $bean, $type, $sql = '', $bindings = array() ) { $sql = $this->writer->glueSQLCondition( $sql ); $rows = $this->relatedRows( $bean, $type, $sql, $bindings ); $links = array(); foreach ( $rows as $key => $row ) { if ( !isset( $links[$row['id']] ) ) $links[$row['id']] = array(); $links[$row['id']][] = $row['linked_by']; unset( $rows[$key]['linked_by'] ); } $beans = $this->oodb->convertToBeans( $type, $rows ); foreach ( $beans as $bean ) $bean->setMeta( 'sys.belongs-to', $links[$bean->id] ); return $beans; } } } namespace RedBeanPHP { use RedBeanPHP\ToolBox as ToolBox; use RedBeanPHP\OODBBean as OODBBean; /** * Bean Helper Interface. * * Interface for Bean Helper. * A little bolt that glues the whole machinery together. * The Bean Helper is passed to the OODB RedBeanPHP Object to * faciliatte the creation of beans and providing them with * a toolbox. The Helper also facilitates the FUSE feature, * determining how beans relate to their models. By overriding * the getModelForBean method you can tune the FUSEing to * fit your business application needs. * * @file RedBeanPHP/IBeanHelper.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. */ interface BeanHelper { /** * Returns a toolbox to empower the bean. * This allows beans to perform OODB operations by themselves, * as such the bean is a proxy for OODB. This allows beans to implement * their magic getters and setters and return lists. * * @return ToolBox */ public function getToolbox(); /** * Does approximately the same as getToolbox but also extracts the * toolbox for you. * This method returns a list with all toolbox items in Toolbox Constructor order: * OODB, adapter, writer and finally the toolbox itself!. * * @return array */ public function getExtractedToolbox(); /** * Given a certain bean this method will * return the corresponding model. * If no model is returned (NULL), RedBeanPHP might ask again. * * @note You can make RedBeanPHP faster by doing the setup wiring yourself. * The event listeners take time, so to speed-up RedBeanPHP you can * drop 'FUSE', if you're not interested in the Models. * * @note You can do funny stuff with this method but please be careful. * You *could* create a model depending on properties of the bean, but * it's a bit well... adventurous, here is an example: * * * class Book extends RedBeanPHP\SimpleModel {}; * class Booklet extends RedBeanPHP\SimpleModel {}; * * class FlexBeanHelper extends RedBeanPHP\BeanHelper\SimpleFacadeBeanHelper { * public function getModelForBean( RedBeanPHP\OODBBean $bean ) { * if (!isset($bean->pages)) return NULL; //will ask again * if ($bean->pages <= 10) return new Booklet; * return new Book; * } * } * * $h = new FlexBeanHelper; * R::getRedBean()->setBeanHelper($h); * $book = R::dispense('book'); * var_dump($book->box()); //NULL cant reach model * $book->pages = 5; * var_dump($book->box()); //Booklet * $book->pages = 15; * var_dump($book->box()); //still.. Booklet, model has been set * $book2 = R::dispense('book'); * $book2->pages = 15; * var_dump($book2->box()); //Book, more than 10 pages * * * @param OODBBean $bean bean to obtain the corresponding model of * * @return SimpleModel|CustomModel|NULL */ public function getModelForBean( OODBBean $bean ); } } namespace RedBeanPHP\BeanHelper { use RedBeanPHP\BeanHelper as BeanHelper; use RedBeanPHP\Facade as Facade; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\SimpleModelHelper as SimpleModelHelper; /** * Bean Helper. * * The Bean helper helps beans to access access the toolbox and * FUSE models. This Bean Helper makes use of the facade to obtain a * reference to the toolbox. * * @file RedBeanPHP/BeanHelperFacade.php * @author Gabor de Mooij and the RedBeanPHP Community * @license BSD/GPLv2 * * @copyright * (c) copyright 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. */ class SimpleFacadeBeanHelper implements BeanHelper { /** * Factory function to create instance of Simple Model, if any. * * @var \Closure */ private static $factory = null; /** * Factory method using a customizable factory function to create * the instance of the Simple Model. * * @param string $modelClassName name of the class * * @return SimpleModel */ public static function factory( $modelClassName ) { $factory = self::$factory; return ( $factory ) ? $factory( $modelClassName ) : new $modelClassName(); } /** * Sets the factory function to create the model when using FUSE * to connect a bean to a model. * * @param \Closure $factory factory function * * @return void */ public static function setFactoryFunction( $factory ) { self::$factory = $factory; } /** * @see BeanHelper::getToolbox */ public function getToolbox() { return Facade::getToolBox(); } /** * @see BeanHelper::getModelForBean */ public function getModelForBean( OODBBean $bean ) { $model = $bean->getMeta( 'type' ); $prefix = defined( 'REDBEAN_MODEL_PREFIX' ) ? REDBEAN_MODEL_PREFIX : '\\Model_'; if ( strpos( $model, '_' ) !== FALSE ) { $modelParts = explode( '_', $model ); $modelName = ''; foreach( $modelParts as $part ) { $modelName .= ucfirst( $part ); } $modelName = $prefix . $modelName; if ( !class_exists( $modelName ) ) { //second try $modelName = $prefix . ucfirst( $model ); if ( !class_exists( $modelName ) ) { return NULL; } } } else { $modelName = $prefix . ucfirst( $model ); if ( !class_exists( $modelName ) ) { return NULL; } } $obj = self::factory( $modelName ); $obj->loadBean( $bean ); return $obj; } /** * @see BeanHelper::getExtractedToolbox */ public function getExtractedToolbox() { return Facade::getExtractedToolbox(); } } } namespace RedBeanPHP { use RedBeanPHP\OODBBean as OODBBean; /** * SimpleModel * Base Model For All RedBeanPHP Models using FUSE. * * RedBeanPHP FUSE is a mechanism to connect beans to posthoc * models. Models are connected to beans by naming conventions. * Actions on beans will result in actions on models. * * @file RedBeanPHP/SimpleModel.php * @author Gabor de Mooij and the RedBeanPHP Team * @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. */ class SimpleModel { /** * @var OODBBean */ protected $bean; /** * Used by FUSE: the ModelHelper class to connect a bean to a model. * This method loads a bean in the model. * * @param OODBBean $bean bean to load * * @return void */ public function loadBean( OODBBean $bean ) { $this->bean = $bean; } /** * Magic Getter to make the bean properties available from * the $this-scope. * * @note this method returns a value, not a reference! * To obtain a reference unbox the bean first! * * @param string $prop property to get * * @return mixed */ public function __get( $prop ) { return $this->bean->$prop; } /** * Magic Setter. * Sets the value directly as a bean property. * * @param string $prop property to set value of * @param mixed $value value to set * * @return void */ public function __set( $prop, $value ) { $this->bean->$prop = $value; } /** * Isset implementation. * Implements the isset function for array-like access. * * @param string $key key to check * * @return boolean */ public function __isset( $key ) { return isset( $this->bean->$key ); } /** * Box the bean using the current model. * This method wraps the current bean in this model. * This method can be reached using FUSE through a simple * OODBBean. The method returns a RedBeanPHP Simple Model. * This is useful if you would like to rely on PHP type hinting. * You can box your beans before passing them to functions or methods * with typed parameters. * * @return SimpleModel */ public function box() { return $this; } /** * Unbox the bean from the model. * This method returns the bean inside the model. * * @return OODBBean */ public function unbox() { return $this->bean; } } } namespace RedBeanPHP { use RedBeanPHP\Observer as Observer; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\Observable as Observable; /** * RedBean Model Helper. * * Connects beans to models. * This is the core of so-called FUSE. * * @file RedBeanPHP/ModelHelper.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. */ class SimpleModelHelper implements Observer { /** * Gets notified by an observable. * This method decouples the FUSE system from the actual beans. * If a FUSE event happens 'update', this method will attempt to * invoke the corresponding method on the bean. * * @param string $eventName i.e. 'delete', 'after_delete' * @param OODBean $bean affected bean * * @return void */ public function onEvent( $eventName, $bean ) { $bean->$eventName(); } /** * Attaches the FUSE event listeners. Now the Model Helper will listen for * CRUD events. If a CRUD event occurs it will send a signal to the model * that belongs to the CRUD bean and this model will take over control from * there. This method will attach the following event listeners to the observable: * * - 'update' (gets called by R::store, before the records gets inserted / updated) * - 'after_update' (gets called by R::store, after the records have been inserted / updated) * - 'open' (gets called by R::load, after the record has been retrieved) * - 'delete' (gets called by R::trash, before deletion of record) * - 'after_delete' (gets called by R::trash, after deletion) * - 'dispense' (gets called by R::dispense) * * For every event type, this method will register this helper as a listener. * The observable will notify the listener (this object) with the event ID and the * affected bean. This helper will then process the event (onEvent) by invoking * the event on the bean. If a bean offers a method with the same name as the * event ID, this method will be invoked. * * @param Observable $observable object to observe * * @return void */ public function attachEventListeners( Observable $observable ) { foreach ( array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ) as $eventID ) { $observable->addEventListener( $eventID, $this ); } } } } namespace RedBeanPHP { use RedBeanPHP\ToolBox as ToolBox; use RedBeanPHP\AssociationManager as AssociationManager; use RedBeanPHP\OODBBean as OODBBean; /** * RedBeanPHP Tag Manager. * * The tag manager offers an easy way to quickly implement basic tagging * functionality. * * Provides methods to tag beans and perform tag-based searches in the * bean database. * * @file RedBeanPHP/TagManager.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. */ class TagManager { /** * @var ToolBox */ protected $toolbox; /** * @var AssociationManager */ protected $associationManager; /** * @var OODBBean */ protected $redbean; /** * Checks if the argument is a comma separated string, in this case * it will split the string into words and return an array instead. * In case of an array the argument will be returned 'as is'. * * @param array|string $tagList list of tags * * @return array */ private function extractTagsIfNeeded( $tagList ) { if ( $tagList !== FALSE && !is_array( $tagList ) ) { $tags = explode( ',', (string) $tagList ); } else { $tags = $tagList; } return $tags; } /** * Finds a tag bean by it's title. * Internal method. * * @param string $title title to search for * * @return OODBBean */ protected function findTagByTitle( $title ) { $beans = $this->redbean->find( 'tag', array( 'title' => array( $title ) ) ); if ( $beans ) { $bean = reset( $beans ); return $bean; } return NULL; } /** * Constructor. * The tag manager offers an easy way to quickly implement basic tagging * functionality. * * @param ToolBox $toolbox toolbox object */ public function __construct( ToolBox $toolbox ) { $this->toolbox = $toolbox; $this->redbean = $toolbox->getRedBean(); $this->associationManager = $this->redbean->getAssociationManager(); } /** * Tests whether a bean has been associated with one ore more * of the listed tags. If the third parameter is TRUE this method * will return TRUE only if all tags that have been specified are indeed * associated with the given bean, otherwise FALSE. * If the third parameter is FALSE this * method will return TRUE if one of the tags matches, FALSE if none * match. * * Tag list can be either an array with tag names or a comma separated list * of tag names. * * @param OODBBean $bean bean to check for tags * @param array|string $tags list of tags * @param boolean $all whether they must all match or just some * * @return boolean */ public function hasTag( $bean, $tags, $all = FALSE ) { $foundtags = $this->tag( $bean ); $tags = $this->extractTagsIfNeeded( $tags ); $same = array_intersect( $tags, $foundtags ); if ( $all ) { return ( implode( ',', $same ) === implode( ',', $tags ) ); } return (bool) ( count( $same ) > 0 ); } /** * Removes all sepcified tags from the bean. The tags specified in * the second parameter will no longer be associated with the bean. * * Tag list can be either an array with tag names or a comma separated list * of tag names. * * @param OODBBean $bean tagged bean * @param array|string $tagList list of tags (names) * * @return void */ public function untag( $bean, $tagList ) { $tags = $this->extractTagsIfNeeded( $tagList ); foreach ( $tags as $tag ) { if ( $t = $this->findTagByTitle( $tag ) ) { $this->associationManager->unassociate( $bean, $t ); } } } /** * Tags a bean or returns tags associated with a bean. * If $tagList is NULL or omitted this method will return a * comma separated list of tags associated with the bean provided. * If $tagList is a comma separated list (string) of tags all tags will * be associated with the bean. * You may also pass an array instead of a string. * * Tag list can be either an array with tag names or a comma separated list * of tag names. * * @param OODBBean $bean bean to be tagged * @param array|string $tagList a list of tags * * @return array */ public function tag( OODBBean $bean, $tagList = NULL ) { if ( is_null( $tagList ) ) { $tags = $bean->sharedTag; $foundTags = array(); foreach ( $tags as $tag ) { $foundTags[] = $tag->title; } return $foundTags; } $this->associationManager->clearRelations( $bean, 'tag' ); $this->addTags( $bean, $tagList ); return $tagList; } /** * Adds tags to a bean. * If $tagList is a comma separated list of tags all tags will * be associated with the bean. * You may also pass an array instead of a string. * * Tag list can be either an array with tag names or a comma separated list * of tag names. * * @param OODBBean $bean bean to add tags to * @param array|string $tagList list of tags to add to bean * * @return void */ public function addTags( OODBBean $bean, $tagList ) { $tags = $this->extractTagsIfNeeded( $tagList ); if ( $tagList === FALSE ) { return; } foreach ( $tags as $tag ) { if ( !$t = $this->findTagByTitle( $tag ) ) { $t = $this->redbean->dispense( 'tag' ); $t->title = $tag; $this->redbean->store( $t ); } $this->associationManager->associate( $bean, $t ); } } /** * Returns all beans that have been tagged with one or more * of the specified tags. * * Tag list can be either an array with tag names or a comma separated list * of tag names. * * @param string $beanType type of bean you are looking for * @param array|string $tagList list of tags to match * @param string $sql additional SQL (use only for pagination) * @param array $bindings bindings * * @return array */ public function tagged( $beanType, $tagList, $sql = '', $bindings = array() ) { $tags = $this->extractTagsIfNeeded( $tagList ); $records = $this->toolbox->getWriter()->queryTagged( $beanType, $tags, FALSE, $sql, $bindings ); return $this->redbean->convertToBeans( $beanType, $records ); } /** * Returns all beans that have been tagged with ALL of the tags given. * * Tag list can be either an array with tag names or a comma separated list * of tag names. * * @param string $beanType type of bean you are looking for * @param array|string $tagList list of tags to match * @param string $sql additional sql snippet * @param array $bindings bindings * * @return array */ public function taggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) { $tags = $this->extractTagsIfNeeded( $tagList ); $records = $this->toolbox->getWriter()->queryTagged( $beanType, $tags, TRUE, $sql, $bindings ); return $this->redbean->convertToBeans( $beanType, $records ); } } } namespace RedBeanPHP { use RedBeanPHP\ToolBox as ToolBox; use RedBeanPHP\OODBBean as OODBBean; /** * Label Maker. * Makes so-called label beans. * A label is a bean with only an id, type and name property. * Labels can be used to create simple entities like categories, tags or enums. * This service class provides convenience methods to deal with this kind of * beans. * * @file RedBeanPHP/LabelMaker.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. */ class LabelMaker { /** * @var ToolBox */ protected $toolbox; /** * Constructor. * * @param ToolBox $toolbox */ public function __construct( ToolBox $toolbox ) { $this->toolbox = $toolbox; } /** * A label is a bean with only an id, type and name property. * This function will dispense beans for all entries in the array. The * values of the array will be assigned to the name property of each * individual bean. * * * $people = R::dispenseLabels( 'person', [ 'Santa', 'Claus' ] ); * * * @param string $type type of beans you would like to have * @param array $labels list of labels, names for each bean * * @return array */ public function dispenseLabels( $type, $labels ) { $labelBeans = array(); foreach ( $labels as $label ) { $labelBean = $this->toolbox->getRedBean()->dispense( $type ); $labelBean->name = $label; $labelBeans[] = $labelBean; } return $labelBeans; } /** * Gathers labels from beans. This function loops through the beans, * collects the value of the name property for each individual bean * and stores the names in a new array. The array then gets sorted using the * default sort function of PHP (sort). * * Usage: * * * $o1->name = 'hamburger'; * $o2->name = 'pizza'; * implode( ',', R::gatherLabels( [ $o1, $o2 ] ) ); //hamburger,pizza * * * Note that the return value is an array of strings, not beans. * * @param array $beans list of beans to loop through * * @return array */ public function gatherLabels( $beans ) { $labels = array(); foreach ( $beans as $bean ) { $labels[] = $bean->name; } sort( $labels ); return $labels; } /** * Fetches an ENUM from the database and creates it if necessary. * An ENUM has the following format: * * * ENUM:VALUE * * * If you pass 'ENUM' only, this method will return an array of its * values: * * * implode( ',', R::gatherLabels( R::enum( 'flavour' ) ) ) //'BANANA,MOCCA' * * * If you pass 'ENUM:VALUE' this method will return the specified enum bean * and create it in the database if it does not exist yet: * * * $bananaFlavour = R::enum( 'flavour:banana' ); * $bananaFlavour->name; * * * So you can use this method to set an ENUM value in a bean: * * * $shake->flavour = R::enum( 'flavour:banana' ); * * * the property flavour now contains the enum bean, a parent bean. * In the database, flavour_id will point to the flavour record with name 'banana'. * * @param string $enum ENUM specification for label * * @return array|OODBBean */ public function enum( $enum ) { $oodb = $this->toolbox->getRedBean(); if ( strpos( $enum, ':' ) === FALSE ) { $type = $enum; $value = FALSE; } else { list( $type, $value ) = explode( ':', $enum ); $value = preg_replace( '/\W+/', '_', strtoupper( trim( $value ) ) ); } /** * We use simply find here, we could use inspect() in fluid mode etc, * but this would be useless. At first sight it looks clean, you could even * bake this into find(), however, find not only has to deal with the primary * search type, people can also include references in the SQL part, so avoiding * find failures does not matter, this is still the quickest way making use * of existing functionality. * * @note There seems to be a bug in XDebug v2.3.2 causing suppressed * exceptions like these to surface anyway, to prevent this use: * * "xdebug.default_enable = 0" * * Also see Github Issue #464 */ $values = $oodb->find( $type ); if ( $value === FALSE ) { return $values; } foreach( $values as $enumItem ) { if ( $enumItem->name === $value ) return $enumItem; } $newEnumItems = $this->dispenseLabels( $type, array( $value ) ); $newEnumItem = reset( $newEnumItems ); $oodb->store( $newEnumItem ); return $newEnumItem; } } } namespace RedBeanPHP { use RedBeanPHP\QueryWriter as QueryWriter; use RedBeanPHP\Adapter\DBAdapter as DBAdapter; use RedBeanPHP\RedException\SQL as SQLException; use RedBeanPHP\Logger as Logger; use RedBeanPHP\Logger\RDefault as RDefault; use RedBeanPHP\Logger\RDefault\Debug as Debug; use RedBeanPHP\Adapter as Adapter; use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; use RedBeanPHP\RedException as RedException; use RedBeanPHP\BeanHelper\SimpleFacadeBeanHelper as SimpleFacadeBeanHelper; use RedBeanPHP\Driver\RPDO as RPDO; use RedBeanPHP\Util\MultiLoader as MultiLoader; use RedBeanPHP\Util\Transaction as Transaction; use RedBeanPHP\Util\Dump as Dump; use RedBeanPHP\Util\DispenseHelper as DispenseHelper; use RedBeanPHP\Util\ArrayTool as ArrayTool; use RedBeanPHP\Util\QuickExport as QuickExport; use RedBeanPHP\Util\MatchUp as MatchUp; use RedBeanPHP\Util\Look as Look; use RedBeanPHP\Util\Diff as Diff; /** * RedBean Facade * * Version Information * RedBean Version @version 5 * * This class hides the object landscape of * RedBeanPHP behind a single letter class providing * almost all functionality with simple static calls. * * @file RedBeanPHP/Facade.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. */ class Facade { /** * RedBeanPHP version constant. */ const C_REDBEANPHP_VERSION = '4.3'; /** * @var ToolBox */ public static $toolbox; /** * @var OODB */ private static $redbean; /** * @var QueryWriter */ private static $writer; /** * @var DBAdapter */ private static $adapter; /** * @var AssociationManager */ private static $associationManager; /** * @var TagManager */ private static $tagManager; /** * @var DuplicationManager */ private static $duplicationManager; /** * @var LabelMaker */ private static $labelMaker; /** * @var Finder */ private static $finder; /** * @var Logger */ private static $logger; /** * @var array */ private static $plugins = array(); /** * @var string */ private static $exportCaseStyle = 'default'; /** * Not in use (backward compatibility SQLHelper) */ public static $f; /** * @var string */ public static $currentDB = ''; /** * @var array */ public static $toolboxes = array(); /** * Internal Query function, executes the desired query. Used by * all facade query functions. This keeps things DRY. * * @param string $method desired query method (i.e. 'cell', 'col', 'exec' etc..) * @param string $sql the sql you want to execute * @param array $bindings array of values to be bound to query statement * * @return array */ private static function query( $method, $sql, $bindings ) { if ( !self::$redbean->isFrozen() ) { try { $rs = Facade::$adapter->$method( $sql, $bindings ); } catch ( SQLException $exception ) { if ( self::$writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ) ,$exception->getDriverDetails() ) ) { return ( $method === 'getCell' ) ? NULL : array(); } else { throw $exception; } } return $rs; } else { return Facade::$adapter->$method( $sql, $bindings ); } } /** * Returns the RedBeanPHP version string. * The RedBeanPHP version string always has the same format "X.Y" * where X is the major version number and Y is the minor version number. * Point releases are not mentioned in the version string. * * @return string */ public static function getVersion() { return self::C_REDBEANPHP_VERSION; } /** * Tests the connection. * Returns TRUE if connection has been established and * FALSE otherwise. * * @return boolean */ public static function testConnection() { if ( !isset( self::$adapter ) ) return FALSE; $database = self::$adapter->getDatabase(); try { @$database->connect(); } catch ( \Exception $e ) {} return $database->isConnected(); } /** * Kickstarts redbean for you. This method should be called before you start using * RedBean. The Setup() method can be called without any arguments, in this case it will * try to create a SQLite database in /tmp called red.db (this only works on UNIX-like systems). * * @param string $dsn Database connection string * @param string $username Username for database * @param string $password Password for database * @param boolean $frozen TRUE if you want to setup in frozen mode * * @return ToolBox */ public static function setup( $dsn = NULL, $username = NULL, $password = NULL, $frozen = FALSE ) { if ( is_null( $dsn ) ) { $dsn = 'sqlite:/' . sys_get_temp_dir() . '/red.db'; } self::addDatabase( 'default', $dsn, $username, $password, $frozen ); self::selectDatabase( 'default' ); return self::$toolbox; } /** * Toggles Narrow Field Mode. * See documentation in QueryWriter. * * @param boolean $mode TRUE = Narrow Field Mode * * @return void */ public static function setNarrowFieldMode( $mode ) { AQueryWriter::setNarrowFieldMode( $mode ); } /** * Wraps a transaction around a closure or string callback. * If an Exception is thrown inside, the operation is automatically rolled back. * If no Exception happens, it commits automatically. * It also supports (simulated) nested transactions (that is useful when * you have many methods that needs transactions but are unaware of * each other). * * Example: * * * $from = 1; * $to = 2; * $amount = 300; * * R::transaction(function() use($from, $to, $amount) * { * $accountFrom = R::load('account', $from); * $accountTo = R::load('account', $to); * $accountFrom->money -= $amount; * $accountTo->money += $amount; * R::store($accountFrom); * R::store($accountTo); * }); * * * @param callable $callback Closure (or other callable) with the transaction logic * * @return mixed */ public static function transaction( $callback ) { return Transaction::transaction( self::$adapter, $callback ); } /** * Adds a database to the facade, afterwards you can select the database using * selectDatabase($key), where $key is the name you assigned to this database. * * Usage: * * * R::addDatabase( 'database-1', 'sqlite:/tmp/db1.txt' ); * R::selectDatabase( 'database-1' ); //to select database again * * * This method allows you to dynamically add (and select) new databases * to the facade. Adding a database with the same key will cause an exception. * * @param string $key ID for the database * @param string $dsn DSN for the database * @param string $user user for connection * @param NULL|string $pass password for connection * @param bool $frozen whether this database is frozen or not * * @return void */ public static function addDatabase( $key, $dsn, $user = NULL, $pass = NULL, $frozen = FALSE ) { if ( isset( self::$toolboxes[$key] ) ) { throw new RedException( 'A database has already been specified for this key.' ); } if ( is_object($dsn) ) { $db = new RPDO( $dsn ); $dbType = $db->getDatabaseType(); } else { $db = new RPDO( $dsn, $user, $pass, TRUE ); $dbType = substr( $dsn, 0, strpos( $dsn, ':' ) ); } $adapter = new DBAdapter( $db ); $writers = array( 'pgsql' => 'PostgreSQL', 'sqlite' => 'SQLiteT', 'cubrid' => 'CUBRID', 'mysql' => 'MySQL', 'sqlsrv' => 'SQLServer', ); $wkey = trim( strtolower( $dbType ) ); if ( !isset( $writers[$wkey] ) ) { $wkey = preg_replace( '/\W/', '' , $wkey ); throw new RedException( 'Unsupported database ('.$wkey.').' ); } $writerClass = '\\RedBeanPHP\\QueryWriter\\'.$writers[$wkey]; $writer = new $writerClass( $adapter ); $redbean = new OODB( $writer, $frozen ); self::$toolboxes[$key] = new ToolBox( $redbean, $adapter, $writer ); } /** * Determines whether a database identified with the specified key has * already been added to the facade. This function will return TRUE * if the database indicated by the key is available and FALSE otherwise. * * @param string $key the key/name of the database to check for * * @return boolean */ public static function hasDatabase( $key ) { return ( isset( self::$toolboxes[$key] ) ); } /** * Selects a different database for the Facade to work with. * If you use the R::setup() you don't need this method. This method is meant * for multiple database setups. This method selects the database identified by the * database ID ($key). Use addDatabase() to add a new database, which in turn * can be selected using selectDatabase(). If you use R::setup(), the resulting * database will be stored under key 'default', to switch (back) to this database * use R::selectDatabase( 'default' ). This method returns TRUE if the database has been * switched and FALSE otherwise (for instance if you already using the specified database). * * @param string $key Key of the database to select * * @return boolean */ public static function selectDatabase( $key ) { if ( self::$currentDB === $key ) { return FALSE; } if ( !isset( self::$toolboxes[$key] ) ) { throw new RedException( 'Database not found in registry. Add database using R::addDatabase().' ); } self::configureFacadeWithToolbox( self::$toolboxes[$key] ); self::$currentDB = $key; return TRUE; } /** * Toggles DEBUG mode. * In Debug mode all SQL that happens under the hood will * be printed to the screen and/or logged. * If no database connection has been configured using R::setup() or * R::selectDatabase() this method will throw an exception. * * There are 2 debug styles: * * Classic: separate parameter bindings, explicit and complete but less readable * Fancy: interpersed bindings, truncates large strings, highlighted schema changes * * Fancy style is more readable but sometimes incomplete. * * The first parameter turns debugging ON or OFF. * The second parameter indicates the mode of operation: * * 0 Log and write to STDOUT classic style (default) * 1 Log only, class style * 2 Log and write to STDOUT fancy style * 3 Log only, fancy style * * This function always returns the logger instance created to generate the * debug messages. * * @param boolean $tf debug mode (TRUE or FALSE) * @param integer $mode mode of operation * * @return RDefault * @throws RedException */ public static function debug( $tf = TRUE, $mode = 0 ) { if ($mode > 1) { $mode -= 2; $logger = new Debug; } else { $logger = new RDefault; } if ( !isset( self::$adapter ) ) { throw new RedException( 'Use R::setup() first.' ); } $logger->setMode($mode); self::$adapter->getDatabase()->setDebugMode( $tf, $logger ); return $logger; } /** * Turns on the fancy debugger. * In 'fancy' mode the debugger will output queries with bound * parameters inside the SQL itself. This method has been added to * offer a convenient way to activate the fancy debugger system * in one call. * * @param boolean $toggle TRUE to activate debugger and select 'fancy' mode * * @return void */ public static function fancyDebug( $toggle = TRUE ) { self::debug( $toggle, 2 ); } /** * Inspects the database schema. If you pass the type of a bean this * method will return the fields of its table in the database. * The keys of this array will be the field names and the values will be * the column types used to store their values. * If no type is passed, this method returns a list of all tables in the database. * * @param string $type Type of bean (i.e. table) you want to inspect * * @return array */ public static function inspect( $type = NULL ) { return ($type === NULL) ? self::$writer->getTables() : self::$writer->getColumns( $type ); } /** * 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. * * @param OODBBean|SimpleModel $bean bean to store * * @return integer|string */ public static function store( $bean ) { return self::$redbean->store( $bean ); } /** * Toggles fluid or frozen mode. In fluid mode the database * structure is adjusted to accomodate your objects. In frozen mode * this is not the case. * * You can also pass an array containing a selection of frozen types. * Let's call this chilly mode, it's just like fluid mode except that * certain types (i.e. tables) aren't touched. * * @param boolean|array $tf mode of operation (TRUE means frozen) */ public static function freeze( $tf = TRUE ) { self::$redbean->freeze( $tf ); } /** * Loads multiple types of beans with the same ID. * This might look like a strange method, however it can be useful * for loading a one-to-one relation. * * Usage: * list( $author, $bio ) = R::loadMulti( 'author, bio', $id ); * * @param string|array $types the set of types to load at once * @param mixed $id the common ID * * @return OODBBean */ public static function loadMulti( $types, $id ) { return MultiLoader::load( self::$redbean, $types, $id ); } /** * Loads a bean from the object database. * It searches for a OODBBean Bean Object in the * database. It does not matter how this bean has been stored. * RedBean uses the primary key ID $id and the string $type * to find the bean. The $type specifies what kind of bean you * are looking for; this is the same type as used with the * dispense() function. If RedBean finds the bean it will return * the OODB Bean object; if it cannot find the bean * RedBean will return a new bean of type $type and with * primary key ID 0. In the latter case it acts basically the * same as dispense(). * * Important note: * If the bean cannot be found in the database a new bean of * the specified type will be generated and returned. * * @param string $type type of bean you want to load * @param integer $id ID of the bean you want to load * @param string $snippet string to use after select (optional) * * @return OODBBean */ public static function load( $type, $id, $snippet = NULL ) { if ( $snippet !== NULL ) self::$writer->setSQLSelectSnippet( $snippet ); $bean = self::$redbean->load( $type, $id ); return $bean; } /** * Same as load, but selects the bean for update, thus locking the bean. * This equals an SQL query like 'SELECT ... FROM ... FOR UPDATE'. * * @param string $type type of bean you want to load * @param integer $id ID of the bean you want to load * * @return OODBBean */ public static function loadForUpdate( $type, $id ) { return self::load( $type, $id, AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); } /** * Removes a bean from the database. * This function will remove the specified OODBBean * Bean Object from the database. * * This facade method also accepts a type-id combination, * in the latter case this method will attempt to load the specified bean * and THEN trash it. * * @param string|OODBBean|SimpleModel $beanOrType bean you want to remove from database * @param integer $id ID if the bean to trash (optional, type-id variant only) * * @return void */ public static function trash( $beanOrType, $id = NULL ) { if ( is_string( $beanOrType ) ) return self::trash( self::load( $beanOrType, $id ) ); return self::$redbean->trash( $beanOrType ); } /** * Dispenses a new RedBean OODB Bean for use with * the rest of the methods. * * @param string|array $typeOrBeanArray type or bean array to import * @param integer $num number of beans to dispense * @param boolean $alwaysReturnArray if TRUE always returns the result as an array * * @return array|OODBBean */ public static function dispense( $typeOrBeanArray, $num = 1, $alwaysReturnArray = FALSE ) { return DispenseHelper::dispense( self::$redbean, $typeOrBeanArray, $num, $alwaysReturnArray ); } /** * Takes a comma separated list of bean types * and dispenses these beans. For each type in the list * you can specify the number of beans to be dispensed. * * Usage: * * * list( $book, $page, $text ) = R::dispenseAll( 'book,page,text' ); * * * This will dispense a book, a page and a text. This way you can * quickly dispense beans of various types in just one line of code. * * Usage: * * * list($book, $pages) = R::dispenseAll('book,page*100'); * * * This returns an array with a book bean and then another array * containing 100 page beans. * * @param string $order a description of the desired dispense order using the syntax above * @param boolean $onlyArrays return only arrays even if amount < 2 * * @return array */ public static function dispenseAll( $order, $onlyArrays = FALSE ) { return DispenseHelper::dispenseAll( self::$redbean, $order, $onlyArrays ); } /** * Convience method. Tries to find beans of a certain type, * if no beans are found, it dispenses a bean of that type. * Note that this function always returns an array. * * @param string $type type of bean you are looking for * @param string $sql SQL code for finding the bean * @param array $bindings parameters to bind to SQL * * @return array */ public static function findOrDispense( $type, $sql = NULL, $bindings = array() ) { DispenseHelper::checkType( $type ); return self::$finder->findOrDispense( $type, $sql, $bindings ); } /** * Same as findOrDispense but returns just one element. * * @param string $type type of bean you are looking for * @param string $sql SQL code for finding the bean * @param array $bindings parameters to bind to SQL * * @return OODBBean */ public static function findOneOrDispense( $type, $sql = NULL, $bindings = array() ) { DispenseHelper::checkType( $type ); $arrayOfBeans = self::findOrDispense( $type, $sql, $bindings ); return reset($arrayOfBeans); } /** * Finds a bean using a type and a where clause (SQL). * As with most Query tools in RedBean you can provide values to * be inserted in the SQL statement by populating the value * array parameter; you can either use the question mark notation * or the slot-notation (:keyname). * * @param string $type the type of bean you are looking for * @param string $sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings array of values to be bound to parameters in query * * @return array */ public static function find( $type, $sql = NULL, $bindings = array() ) { return self::$finder->find( $type, $sql, $bindings ); } /** * The findAll() method differs from the find() method in that it does * not assume a WHERE-clause, so this is valid: * * R::findAll('person',' ORDER BY name DESC '); * * Your SQL does not have to start with a valid WHERE-clause condition. * * @param string $type the type of bean you are looking for * @param string $sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings array of values to be bound to parameters in query * * @return array */ public static function findAll( $type, $sql = NULL, $bindings = array() ) { return self::$finder->find( $type, $sql, $bindings ); } /** * Like find() but also exports the beans as an array. * This method will perform a find-operation. For every bean * in the result collection this method will call the export() method. * This method returns an array containing the array representations * of every bean in the result set. * * @see Finder::find * * @param string $type type the type of bean you are looking for * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return array */ public static function findAndExport( $type, $sql = NULL, $bindings = array() ) { return self::$finder->findAndExport( $type, $sql, $bindings ); } /** * Like R::find() but returns the first bean only. * * @param string $type the type of bean you are looking for * @param string $sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings array of values to be bound to parameters in query * * @return OODBBean */ public static function findOne( $type, $sql = NULL, $bindings = array() ) { return self::$finder->findOne( $type, $sql, $bindings ); } /** * Like find() but returns the last bean of the result array. * Opposite of Finder::findLast(). * If no beans are found, this method will return NULL. * * @see Finder::find * * @param string $type the type of bean you are looking for * @param string $sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return OODBBean */ public static function findLast( $type, $sql = NULL, $bindings = array() ) { return self::$finder->findLast( $type, $sql, $bindings ); } /** * Finds a BeanCollection using the repository. * A bean collection can be used to retrieve one bean at a time using * cursors - this is useful for processing large datasets. A bean collection * will not load all beans into memory all at once, just one at a time. * * @param string $type the type of bean you are looking for * @param string $sql SQL query to find the desired bean, starting right after WHERE clause * @param array $bindings values array of values to be bound to parameters in query * * @return BeanCollection */ public static function findCollection( $type, $sql = NULL, $bindings = array() ) { return self::$finder->findCollection( $type, $sql, $bindings ); } /** * Returns a hashmap with bean arrays keyed by type using an SQL * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review' * this method will return movie and review beans. * * Example: * * * $stuff = $finder->findMulti('movie,review', ' * SELECT movie.*, review.* FROM movie * LEFT JOIN review ON review.movie_id = movie.id'); * * * After this operation, $stuff will contain an entry 'movie' containing all * movies and an entry named 'review' containing all reviews (all beans). * You can also pass bindings. * * If you want to re-map your beans, so you can use $movie->ownReviewList without * having RedBeanPHP executing an SQL query you can use the fourth parameter to * define a selection of remapping closures. * * The remapping argument (optional) should contain an array of arrays. * Each array in the remapping array should contain the following entries: * * * array( * 'a' => TYPE A * 'b' => TYPE B * 'matcher' => MATCHING FUNCTION ACCEPTING A, B and ALL BEANS * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS * ) * * * Using this mechanism you can build your own 'preloader' with tiny function * snippets (and those can be re-used and shared online of course). * * Example: * * * array( * 'a' => 'movie' //define A as movie * 'b' => 'review' //define B as review * 'matcher' => function( $a, $b ) { * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id * } * 'do' => function( $a, $b ) { * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'. * } * ) * * * @note the SQL query provided IS NOT THE ONE used internally by this function, * this function will pre-process the query to get all the data required to find the beans. * * @note if you use the 'book.*' notation make SURE you're * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because * it's actually an SQL-like template SLOT, not real SQL. * * @note instead of an SQL query you can pass a result array as well. * * @param string|array $types a list of types (either array or comma separated string) * @param string|array $sql an SQL query or an array of prefetched records * @param array $bindings optional, bindings for SQL query * @param array $remappings optional, an array of remapping arrays * * @return array */ public static function findMulti( $types, $sql, $bindings = array(), $remappings = array() ) { return self::$finder->findMulti( $types, $sql, $bindings, $remappings ); } /** * 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 static function batch( $type, $ids ) { return self::$redbean->batch( $type, $ids ); } /** * Alias for batch(). Batch method is older but since we added so-called *All * methods like storeAll, trashAll, dispenseAll and findAll it seemed logical to * improve the consistency of the Facade API and also add an alias for batch() called * loadAll. * * @param string $type type of beans * @param array $ids ids to load * * @return array */ public static function loadAll( $type, $ids ) { return self::$redbean->batch( $type, $ids ); } /** * Convenience function to execute Queries directly. * Executes SQL. * * @param string $sql SQL query to execute * @param array $bindings a list of values to be bound to query parameters * * @return integer */ public static function exec( $sql, $bindings = array() ) { return self::query( 'exec', $sql, $bindings ); } /** * Convenience function to fire an SQL query using the RedBeanPHP * database adapter. This method allows you to directly query the * database without having to obtain an database adapter instance first. * Executes the specified SQL query together with the specified * parameter bindings and returns all rows * and all columns. * * @param string $sql SQL query to execute * @param array $bindings a list of values to be bound to query parameters * * @return array */ public static function getAll( $sql, $bindings = array() ) { return self::query( 'get', $sql, $bindings ); } /** * Convenience function to fire an SQL query using the RedBeanPHP * database adapter. This method allows you to directly query the * database without having to obtain an database adapter instance first. * Executes the specified SQL query together with the specified * parameter bindings and returns a single cell. * * @param string $sql SQL query to execute * @param array $bindings a list of values to be bound to query parameters * * @return string */ public static function getCell( $sql, $bindings = array() ) { return self::query( 'getCell', $sql, $bindings ); } /** * Convenience function to fire an SQL query using the RedBeanPHP * database adapter. This method allows you to directly query the * database without having to obtain an database adapter instance first. * Executes the specified SQL query together with the specified * parameter bindings and returns a single row. * * @param string $sql SQL query to execute * @param array $bindings a list of values to be bound to query parameters * * @return array */ public static function getRow( $sql, $bindings = array() ) { return self::query( 'getRow', $sql, $bindings ); } /** * Convenience function to fire an SQL query using the RedBeanPHP * database adapter. This method allows you to directly query the * database without having to obtain an database adapter instance first. * Executes the specified SQL query together with the specified * parameter bindings and returns a single column. * * @param string $sql SQL query to execute * @param array $bindings a list of values to be bound to query parameters * * @return array */ public static function getCol( $sql, $bindings = array() ) { return self::query( 'getCol', $sql, $bindings ); } /** * Convenience function to execute Queries directly. * Executes SQL. * Results will be returned as an associative array. The first * column in the select clause will be used for the keys in this array and * the second column will be used for the values. If only one column is * selected in the query, both key and value of the array will have the * value of this field for each row. * * @param string $sql SQL query to execute * @param array $bindings a list of values to be bound to query parameters * * @return array */ public static function getAssoc( $sql, $bindings = array() ) { return self::query( 'getAssoc', $sql, $bindings ); } /** *Convenience function to fire an SQL query using the RedBeanPHP * database adapter. This method allows you to directly query the * database without having to obtain an database adapter instance first. * Executes the specified SQL query together with the specified * parameter bindings and returns an associative array. * Results will be returned as an associative array indexed by the first * column in the select. * * @param string $sql SQL query to execute * @param array $bindings a list of values to be bound to query parameters * * @return array */ public static function getAssocRow( $sql, $bindings = array() ) { return self::query( 'getAssocRow', $sql, $bindings ); } /** * Returns the insert ID for databases that support/require this * functionality. Alias for R::getAdapter()->getInsertID(). * * @return mixed */ public static function getInsertID() { return self::$adapter->getInsertID(); } /** * Makes a copy of a bean. This method makes a deep copy * of the bean.The copy will have the following features. * - All beans in own-lists will be duplicated as well * - All references to shared beans will be copied but not the shared beans themselves * - All references to parent objects (_id fields) will be copied but not the parents themselves * In most cases this is the desired scenario for copying beans. * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found * (i.e. one that already has been processed) the ID of the bean will be returned. * This should not happen though. * * Note: * This function does a reflectional database query so it may be slow. * * @deprecated * This function is deprecated in favour of R::duplicate(). * This function has a confusing method signature, the R::duplicate() function * only accepts two arguments: bean and filters. * * @param OODBBean $bean bean to be copied * @param array $trail for internal usage, pass array() * @param boolean $pid for internal usage * @param array $filters white list filter with bean types to duplicate * * @return array */ public static function dup( $bean, $trail = array(), $pid = FALSE, $filters = array() ) { self::$duplicationManager->setFilters( $filters ); return self::$duplicationManager->dup( $bean, $trail, $pid ); } /** * Makes a deep copy of a bean. This method makes a deep copy * of the bean.The copy will have the following: * * * All beans in own-lists will be duplicated as well * * All references to shared beans will be copied but not the shared beans themselves * * All references to parent objects (_id fields) will be copied but not the parents themselves * * In most cases this is the desired scenario for copying beans. * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found * (i.e. one that already has been processed) the ID of the bean will be returned. * This should not happen though. * * Note: * This function does a reflectional database query so it may be slow. * * Note: * This is a simplified version of the deprecated R::dup() function. * * @param OODBBean $bean bean to be copied * @param array $white white list filter with bean types to duplicate * * @return array */ public static function duplicate( $bean, $filters = array() ) { return self::dup( $bean, array(), FALSE, $filters ); } /** * Exports a collection of beans. Handy for XML/JSON exports with a * Javascript framework like Dojo or ExtJS. * What will be exported: * * * contents of the bean * * all own bean lists (recursively) * * all shared beans (not THEIR own lists) * * @param array|OODBBean $beans beans to be exported * @param boolean $parents whether you want parent beans to be exported * @param array $filters whitelist of types * * @return array */ public static function exportAll( $beans, $parents = FALSE, $filters = array()) { return self::$duplicationManager->exportAll( $beans, $parents, $filters, self::$exportCaseStyle ); } /** * Selects case style for export. * This will determine the case style for the keys of exported beans (see exportAll). * The following options are accepted: * * * 'default' RedBeanPHP by default enforces Snake Case (i.e. book_id is_valid ) * * 'camel' Camel Case (i.e. bookId isValid ) * * 'dolphin' Dolphin Case (i.e. bookID isValid ) Like CamelCase but ID is written all uppercase * * @warning RedBeanPHP transforms camelCase to snake_case using a slightly different * algorithm, it also converts isACL to is_acl (not is_a_c_l) and bookID to book_id. * Due to information loss this cannot be corrected. However if you might try * DolphinCase for IDs it takes into account the exception concerning IDs. * * @param string $caseStyle case style identifier * * @return void */ public static function useExportCase( $caseStyle = 'default' ) { if ( !in_array( $caseStyle, array( 'default', 'camel', 'dolphin' ) ) ) throw new RedException( 'Invalid case selected.' ); self::$exportCaseStyle = $caseStyle; } /** * Converts a series of rows to beans. * This method converts a series of rows to beans. * The type of the desired output beans can be specified in the * first parameter. The second parameter is meant for the database * result rows. * * Usage: * * * $rows = R::getAll( 'SELECT * FROM ...' ) * $beans = R::convertToBeans( $rows ); * * * As of version 4.3.2 you can specify a meta-mask. * Data from columns with names starting with the value specified in the mask * will be transferred to the meta section of a bean (under data.bundle). * * * $rows = R::getAll( 'SELECT FROM... COUNT(*) AS extra_count ...' ); * $beans = R::convertToBeans( $rows ); * $bean = reset( $beans ); * $data = $bean->getMeta( 'data.bundle' ); * $extra_count = $data['extra_count']; * * * 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 whos name starts with $mask will be * transferred to the meta section of the bean under key 'data.bundle'. * * @param string $type type of beans to produce * @param array $rows must contain an array of array * @param string $metamask meta mask to apply (optional) * * @return array */ public static function convertToBeans( $type, $rows, $metamask = NULL ) { return self::$redbean->convertToBeans( $type, $rows, $metamask ); } /** * Just like converToBeans, but for one bean. * * @param string $type type of bean to produce * @param array $row one row from the database * @param string $metamask metamask (see convertToBeans) * * @return array */ public static function convertToBean( $type, $row, $metamask = NULL ) { $beans = self::$redbean->convertToBeans( $type, array( $row ), $metamask ); $bean = reset( $beans ); return $bean; } /** * Part of RedBeanPHP Tagging API. * Tests whether a bean has been associated with one ore more * of the listed tags. If the third parameter is TRUE this method * will return TRUE only if all tags that have been specified are indeed * associated with the given bean, otherwise FALSE. * If the third parameter is FALSE this * method will return TRUE if one of the tags matches, FALSE if none * match. * * @param OODBBean $bean bean to check for tags * @param array $tags list of tags * @param boolean $all whether they must all match or just some * * @return boolean */ public static function hasTag( $bean, $tags, $all = FALSE ) { return self::$tagManager->hasTag( $bean, $tags, $all ); } /** * Part of RedBeanPHP Tagging API. * Removes all specified tags from the bean. The tags specified in * the second parameter will no longer be associated with the bean. * * @param OODBBean $bean tagged bean * @param array $tagList list of tags (names) * * @return void */ public static function untag( $bean, $tagList ) { self::$tagManager->untag( $bean, $tagList ); } /** * Part of RedBeanPHP Tagging API. * Tags a bean or returns tags associated with a bean. * If $tagList is NULL or omitted this method will return a * comma separated list of tags associated with the bean provided. * If $tagList is a comma separated list (string) of tags all tags will * be associated with the bean. * You may also pass an array instead of a string. * * @param OODBBean $bean bean to tag * @param mixed $tagList tags to attach to the specified bean * * @return string */ public static function tag( OODBBean $bean, $tagList = NULL ) { return self::$tagManager->tag( $bean, $tagList ); } /** * Part of RedBeanPHP Tagging API. * Adds tags to a bean. * If $tagList is a comma separated list of tags all tags will * be associated with the bean. * You may also pass an array instead of a string. * * @param OODBBean $bean bean to tag * @param array $tagList list of tags to add to bean * * @return void */ public static function addTags( OODBBean $bean, $tagList ) { self::$tagManager->addTags( $bean, $tagList ); } /** * Part of RedBeanPHP Tagging API. * Returns all beans that have been tagged with one of the tags given. * * @param string $beanType type of bean you are looking for * @param array $tagList list of tags to match * @param string $sql additional SQL query snippet * @param array $bindings a list of values to bind to the query parameters * * @return array */ public static function tagged( $beanType, $tagList, $sql = '', $bindings = array() ) { return self::$tagManager->tagged( $beanType, $tagList, $sql, $bindings ); } /** * Part of RedBeanPHP Tagging API. * Returns all beans that have been tagged with ALL of the tags given. * * @param string $beanType type of bean you are looking for * @param array $tagList list of tags to match * @param string $sql additional SQL query snippet * @param array $bindings a list of values to bind to the query parameters * * @return array */ public static function taggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) { return self::$tagManager->taggedAll( $beanType, $tagList, $sql, $bindings ); } /** * Wipes all beans of type $beanType. * * @param string $beanType type of bean you want to destroy entirely * * @return boolean */ public static function wipe( $beanType ) { return Facade::$redbean->wipe( $beanType ); } /** * 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 static function count( $type, $addSQL = '', $bindings = array() ) { return Facade::$redbean->count( $type, $addSQL, $bindings ); } /** * Configures the facade, want to have a new Writer? A new Object Database or a new * Adapter and you want it on-the-fly? Use this method to hot-swap your facade with a new * toolbox. * * @param ToolBox $tb toolbox to configure facade with * * @return ToolBox */ public static function configureFacadeWithToolbox( ToolBox $tb ) { $oldTools = self::$toolbox; self::$toolbox = $tb; self::$writer = self::$toolbox->getWriter(); self::$adapter = self::$toolbox->getDatabaseAdapter(); self::$redbean = self::$toolbox->getRedBean(); self::$finder = new Finder( self::$toolbox ); self::$associationManager = new AssociationManager( self::$toolbox ); self::$redbean->setAssociationManager( self::$associationManager ); self::$labelMaker = new LabelMaker( self::$toolbox ); $helper = new SimpleModelHelper(); $helper->attachEventListeners( self::$redbean ); self::$redbean->setBeanHelper( new SimpleFacadeBeanHelper ); self::$duplicationManager = new DuplicationManager( self::$toolbox ); self::$tagManager = new TagManager( self::$toolbox ); return $oldTools; } /** * Facade Convience method for adapter transaction system. * Begins a transaction. * * @return bool */ public static function begin() { if ( !self::$redbean->isFrozen() ) return FALSE; self::$adapter->startTransaction(); return TRUE; } /** * Facade Convience method for adapter transaction system. * Commits a transaction. * * @return bool */ public static function commit() { if ( !self::$redbean->isFrozen() ) return FALSE; self::$adapter->commit(); return TRUE; } /** * Facade Convience method for adapter transaction system. * Rolls back a transaction. * * @return bool */ public static function rollback() { if ( !self::$redbean->isFrozen() ) return FALSE; self::$adapter->rollback(); return TRUE; } /** * Returns a list of columns. Format of this array: * array( fieldname => type ) * Note that this method only works in fluid mode because it might be * quite heavy on production servers! * * @param string $table name of the table (not type) you want to get columns of * * @return array */ public static function getColumns( $table ) { return self::$writer->getColumns( $table ); } /** * Generates question mark slots for an array of values. * * @param array $array array to generate question mark slots for * @param string $template template to use * * @return string */ public static function genSlots( $array, $template = NULL ) { return ArrayTool::genSlots( $array, $template ); } /** * Flattens a multi dimensional bindings array for use with genSlots(). * * @param array $array array to flatten * @param array $result result array parameter (for recursion) * * @return array */ public static function flat( $array, $result = array() ) { return ArrayTool::flat( $array, $result ); } /** * Nukes the entire database. * This will remove all schema structures from the database. * Only works in fluid mode. Be careful with this method. * * @warning dangerous method, will remove all tables, columns etc. * * @return void */ public static function nuke() { if ( !self::$redbean->isFrozen() ) { self::$writer->wipeAll(); } } /** * Short hand function to store a set of beans at once, IDs will be * returned as an array. For information please consult the R::store() * function. * A loop saver. * * @param array $beans list of beans to be stored * * @return array */ public static function storeAll( $beans ) { $ids = array(); foreach ( $beans as $bean ) { $ids[] = self::store( $bean ); } return $ids; } /** * Short hand function to trash a set of beans at once. * For information please consult the R::trash() function. * A loop saver. * * @param array $beans list of beans to be trashed * * @return void */ public static function trashAll( $beans ) { foreach ( $beans as $bean ) { self::trash( $bean ); } } /** * Toggles Writer Cache. * Turns the Writer Cache on or off. The Writer Cache is a simple * query based caching system that may improve performance without the need * for cache management. This caching system will cache non-modifying queries * that are marked with special SQL comments. As soon as a non-marked query * gets executed the cache will be flushed. Only non-modifying select queries * have been marked therefore this mechanism is a rather safe way of caching, requiring * no explicit flushes or reloads. Of course this does not apply if you intend to test * or simulate concurrent querying. * * @param boolean $yesNo TRUE to enable cache, FALSE to disable cache * * @return void */ public static function useWriterCache( $yesNo ) { self::getWriter()->setUseCache( $yesNo ); } /** * A label is a bean with only an id, type and name property. * This function will dispense beans for all entries in the array. The * values of the array will be assigned to the name property of each * individual bean. * * @param string $type type of beans you would like to have * @param array $labels list of labels, names for each bean * * @return array */ public static function dispenseLabels( $type, $labels ) { return self::$labelMaker->dispenseLabels( $type, $labels ); } /** * Generates and returns an ENUM value. This is how RedBeanPHP handles ENUMs. * Either returns a (newly created) bean respresenting the desired ENUM * value or returns a list of all enums for the type. * * To obtain (and add if necessary) an ENUM value: * * * $tea->flavour = R::enum( 'flavour:apple' ); * * * Returns a bean of type 'flavour' with name = apple. * This will add a bean with property name (set to APPLE) to the database * if it does not exist yet. * * To obtain all flavours: * * * R::enum('flavour'); * * * To get a list of all flavour names: * * * R::gatherLabels( R::enum( 'flavour' ) ); * * * @param string $enum either type or type-value * * @return array|OODBBean */ public static function enum( $enum ) { return self::$labelMaker->enum( $enum ); } /** * Gathers labels from beans. This function loops through the beans, * collects the values of the name properties of each individual bean * and stores the names in a new array. The array then gets sorted using the * default sort function of PHP (sort). * * @param array $beans list of beans to loop * * @return array */ public static function gatherLabels( $beans ) { return self::$labelMaker->gatherLabels( $beans ); } /** * Closes the database connection. * * @return void */ public static function close() { if ( isset( self::$adapter ) ) { self::$adapter->close(); } } /** * Simple convenience function, returns ISO date formatted representation * of $time. * * @param mixed $time UNIX timestamp * * @return string */ public static function isoDate( $time = NULL ) { if ( !$time ) { $time = time(); } return @date( 'Y-m-d', $time ); } /** * Simple convenience function, returns ISO date time * formatted representation * of $time. * * @param mixed $time UNIX timestamp * * @return string */ public static function isoDateTime( $time = NULL ) { if ( !$time ) $time = time(); return @date( 'Y-m-d H:i:s', $time ); } /** * Optional accessor for neat code. * Sets the database adapter you want to use. * * @param Adapter $adapter Database Adapter for facade to use * * @return void */ public static function setDatabaseAdapter( Adapter $adapter ) { self::$adapter = $adapter; } /** * Optional accessor for neat code. * Sets the database adapter you want to use. * * @param QueryWriter $writer Query Writer instance for facade to use * * @return void */ public static function setWriter( QueryWriter $writer ) { self::$writer = $writer; } /** * Optional accessor for neat code. * Sets the database adapter you want to use. * * @param OODB $redbean Object Database for facade to use */ public static function setRedBean( OODB $redbean ) { self::$redbean = $redbean; } /** * Optional accessor for neat code. * Sets the database adapter you want to use. * * @return DBAdapter */ public static function getDatabaseAdapter() { return self::$adapter; } /** * In case you use PDO (which is recommended and the default but not mandatory, hence * the database adapter), you can use this method to obtain the PDO object directly. * This is a convenience method, it will do the same as: * * * R::getDatabaseAdapter()->getDatabase()->getPDO(); * * * If the PDO object could not be found, for whatever reason, this method * will return NULL instead. * * @return NULL|PDO */ public static function getPDO() { $databaseAdapter = self::getDatabaseAdapter(); if ( is_null( $databaseAdapter ) ) return NULL; $database = $databaseAdapter->getDatabase(); if ( is_null( $database ) ) return NULL; if ( !method_exists( $database, 'getPDO' ) ) return NULL; return $database->getPDO(); } /** * Returns the current duplication manager instance. * * @return DuplicationManager */ public static function getDuplicationManager() { return self::$duplicationManager; } /** * Optional accessor for neat code. * Sets the database adapter you want to use. * * @return QueryWriter */ public static function getWriter() { return self::$writer; } /** * Optional accessor for neat code. * Sets the database adapter you want to use. * * @return OODB */ public static function getRedBean() { return self::$redbean; } /** * Returns the toolbox currently used by the facade. * To set the toolbox use R::setup() or R::configureFacadeWithToolbox(). * To create a toolbox use Setup::kickstart(). Or create a manual * toolbox using the ToolBox class. * * @return ToolBox */ public static function getToolBox() { return self::$toolbox; } /** * Mostly for internal use, but might be handy * for some users. * This returns all the components of the currently * selected toolbox. * * Returns the components in the following order: * * # OODB instance (getRedBean()) * # Database Adapter * # Query Writer * # Toolbox itself * * @return array */ public static function getExtractedToolbox() { return array( self::$redbean, self::$adapter, self::$writer, self::$toolbox ); } /** * Facade method for AQueryWriter::renameAssociation() * * @param string|array $from * @param string $to * * @return void */ public static function renameAssociation( $from, $to = NULL ) { AQueryWriter::renameAssociation( $from, $to ); } /** * Little helper method for Resty Bean Can server and others. * Takes an array of beans and exports each bean. * Unlike exportAll this method does not recurse into own lists * and shared lists, the beans are exported as-is, only loaded lists * are exported. * * @param array $beans beans * * @return array */ public static function beansToArray( $beans ) { $list = array(); foreach( $beans as $bean ) $list[] = $bean->export(); return $list; } /** * Sets the error mode for FUSE. * What to do if a FUSE model method does not exist? * You can set the following options: * * * OODBBean::C_ERR_IGNORE (default), ignores the call, returns NULL * * OODBBean::C_ERR_LOG, logs the incident using error_log * * OODBBean::C_ERR_NOTICE, triggers a E_USER_NOTICE * * OODBBean::C_ERR_WARN, triggers a E_USER_WARNING * * OODBBean::C_ERR_EXCEPTION, throws an exception * * OODBBean::C_ERR_FUNC, allows you to specify a custom handler (function) * * OODBBean::C_ERR_FATAL, triggers a E_USER_ERROR * * * Custom handler method signature: handler( array ( * 'message' => string * 'bean' => OODBBean * 'method' => string * ) ) * * * This method returns the old mode and handler as an array. * * @param integer $mode mode, determines how to handle errors * @param callable|NULL $func custom handler (if applicable) * * @return array */ public static function setErrorHandlingFUSE( $mode, $func = NULL ) { return OODBBean::setErrorHandlingFUSE( $mode, $func ); } /** * Simple but effective debug function. * Given a one or more beans this method will * return an array containing first part of the string * representation of each item in the array. * * @param OODBBean|array $data either a bean or an array of beans * * @return array */ public static function dump( $data ) { return Dump::dump( $data ); } /** * Binds an SQL function to a column. * This method can be used to setup a decode/encode scheme or * perform UUID insertion. This method is especially useful for handling * MySQL spatial columns, because they need to be processed first using * the asText/GeomFromText functions. * * Example: * * * R::bindFunc( 'read', 'location.point', 'asText' ); * R::bindFunc( 'write', 'location.point', 'GeomFromText' ); * * * Passing NULL as the function will reset (clear) the function * for this column/mode. * * @param string $mode mode for function: i.e. read or write * @param string $field field (table.column) to bind function to * @param string $function SQL function to bind to specified column * * @return void */ public static function bindFunc( $mode, $field, $function ) { self::$redbean->bindFunc( $mode, $field, $function ); } /** * Sets global aliases. * Registers a batch of aliases in one go. This works the same as * fetchAs and setAutoResolve but explicitly. For instance if you register * the alias 'cover' for 'page' a property containing a reference to a * page bean called 'cover' will correctly return the page bean and not * a (non-existant) cover bean. * * * R::aliases( array( 'cover' => 'page' ) ); * $book = R::dispense( 'book' ); * $page = R::dispense( 'page' ); * $book->cover = $page; * R::store( $book ); * $book = $book->fresh(); * $cover = $book->cover; * echo $cover->getMeta( 'type' ); //page * * * The format of the aliases registration array is: * * {alias} => {actual type} * * In the example above we use: * * cover => page * * From that point on, every bean reference to a cover * will return a 'page' bean. Note that with autoResolve this * feature along with fetchAs() is no longer very important, although * relying on explicit aliases can be a bit faster. * * @param array $list list of global aliases to use * * @return void */ public static function aliases( $list ) { OODBBean::aliases( $list ); } /** * Tries to find a bean matching a certain type and * criteria set. If no beans are found a new bean * will be created, the criteria will be imported into this * bean and the bean will be stored and returned. * If multiple beans match the criteria only the first one * will be returned. * * @param string $type type of bean to search for * @param array $like criteria set describing the bean to search for * * @return OODBBean */ public static function findOrCreate( $type, $like = array() ) { return self::$finder->findOrCreate( $type, $like ); } /** * Tries to find beans matching the specified type and * criteria set. * * If the optional additional SQL snippet is a condition, it will * be glued to the rest of the query using the AND operator. * * @param string $type type of bean to search for * @param array $like optional criteria set describing the bean to search for * @param string $sql optional additional SQL for sorting * * @return array */ public static function findLike( $type, $like = array(), $sql = '' ) { return self::$finder->findLike( $type, $like, $sql ); } /** * Starts logging queries. * Use this method to start logging SQL queries being * executed by the adapter. * * @note you cannot use R::debug and R::startLogging * at the same time because R::debug is essentially a * special kind of logging. * * @return void */ public static function startLogging() { self::debug( TRUE, RDefault::C_LOGGER_ARRAY ); } /** * Stops logging, comfortable method to stop logging of queries. * * @return void */ public static function stopLogging() { self::debug( FALSE ); } /** * Returns the log entries written after the startLogging. * * @return array */ public static function getLogs() { return self::getLogger()->getLogs(); } /** * Resets the Query counter. * * @return integer */ public static function resetQueryCount() { self::$adapter->getDatabase()->resetCounter(); } /** * Returns the number of SQL queries processed. * * @return integer */ public static function getQueryCount() { return self::$adapter->getDatabase()->getQueryCount(); } /** * Returns the current logger instance being used by the * database object. * * @return Logger */ public static function getLogger() { return self::$adapter->getDatabase()->getLogger(); } /** * Alias for setAutoResolve() method on OODBBean. * Enables or disables auto-resolving fetch types. * Auto-resolving aliased parent beans is convenient but can * be slower and can create infinite recursion if you * used aliases to break cyclic relations in your domain. * * @param boolean $automatic TRUE to enable automatic resolving aliased parents * * @return void */ public static function setAutoResolve( $automatic = TRUE ) { OODBBean::setAutoResolve( (boolean) $automatic ); } /** * 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 mixed */ public static function usePartialBeans( $yesNoBeans ) { return self::$redbean->getCurrentRepository()->usePartialBeans( $yesNoBeans ); } /** * Exposes the result of the specified SQL query as a CSV file. * Usage: * * R::csv( 'SELECT * `name`, * population * FROM city * WHERE region = :region ', * array( ':region' => 'Denmark' ), * array( 'city', 'population' ), * '/tmp/cities.csv' * ); * * The command above will select all cities in Denmark * and create a CSV with columns 'city' and 'population' and * populate the cells under these column headers with the * names of the cities and the population numbers respectively. * * @param string $sql SQL query to expose result of * @param array $bindings parameter bindings * @param array $columns column headers for CSV file * @param string $path path to save CSV file to * @param boolean $output TRUE to output CSV directly using readfile * @param array $options delimiter, quote and escape character respectively * * @return void */ public static function csv( $sql = '', $bindings = array(), $columns = NULL, $path = '/tmp/redexport_%s.csv', $output = true ) { $quickExport = new QuickExport( self::$toolbox ); $quickExport->csv( $sql, $bindings, $columns, $path, $output ); } /** * MatchUp is a powerful productivity boosting method that can replace simple control * scripts with a single RedBeanPHP command. Typically, matchUp() is used to * replace login scripts, token generation scripts and password reset scripts. * The MatchUp method takes a bean type, an SQL query snippet (starting at the WHERE clause), * SQL bindings, a pair of task arrays and a bean reference. * * If the first 3 parameters match a bean, the first task list will be considered, * otherwise the second one will be considered. On consideration, each task list, * an array of keys and values will be executed. Every key in the task list should * correspond to a bean property while every value can either be an expression to * be evaluated or a closure (PHP 5.3+). After applying the task list to the bean * it will be stored. If no bean has been found, a new bean will be dispensed. * * This method will return TRUE if the bean was found and FALSE if not AND * there was a NOT-FOUND task list. If no bean was found AND there was also * no second task list, NULL will be returned. * * @param string $type type of bean you're looking for * @param string $sql SQL snippet (starting at the WHERE clause, omit WHERE-keyword) * @param array $bindings array of parameter bindings for SQL snippet * @param array $onFoundDo task list to be considered on finding the bean * @param array $onNotFoundDo task list to be considered on NOT finding the bean * @param OODBBean &$bean reference to obtain the found bean * * @return mixed */ public static function matchUp( $type, $sql, $bindings = array(), $onFoundDo = NULL, $onNotFoundDo = NULL, &$bean = NULL ) { $matchUp = new MatchUp( self::$toolbox ); return $matchUp->matchUp( $type, $sql, $bindings, $onFoundDo, $onNotFoundDo, $bean ); } /** * Returns an instance of the Look Helper class. * The instance will be configured with the current toolbox. * * @return Look */ public static function getLook() { return new Look( self::$toolbox ); } /** * Calculates a diff between two beans (or arrays of beans). * The result of this method is an array describing the differences of the second bean compared to * the first, where the first bean is taken as reference. The array is keyed by type/property, id and property name, where * type/property is either the type (in case of the root bean) or the property of the parent bean where the type resides. * The diffs are mainly intended for logging, you cannot apply these diffs as patches to other beans. * However this functionality might be added in the future. * * The keys of the array can be formatted using the $format parameter. * A key will be composed of a path (1st), id (2nd) and property (3rd). * Using printf-style notation you can determine the exact format of the key. * The default format will look like: * * 'book.1.title' => array( , ) * * If you only want a simple diff of one bean and you don't care about ids, * you might pass a format like: '%1$s.%3$s' which gives: * * 'book.1.title' => array( , ) * * The filter parameter can be used to set filters, it should be an array * of property names that have to be skipped. By default this array is filled with * two strings: 'created' and 'modified'. * * @param OODBBean|array $bean reference beans * @param OODBBean|array $other beans to compare * @param array $filters names of properties of all beans to skip * @param string $format the format of the key, defaults to '%s.%s.%s' * @param string $type type/property of bean to use for key generation * * @return array */ public static function diff( $bean, $other, $filters = array( 'created', 'modified' ), $pattern = '%s.%s.%s' ) { $diff = new Diff( self::$toolbox ); return $diff->diff( $bean, $other, $filters, $pattern ); } /** * Toggles JSON column features. * Invoking this method with boolean TRUE causes 2 JSON features to be enabled. * Beans will automatically JSONify any array that's not in a list property and * the Query Writer (if capable) will attempt to create a JSON column for strings that * appear to contain JSON. * * Feature #1: * AQueryWriter::useJSONColumns * * Toggles support for automatic generation of JSON columns. * Using JSON columns means that strings containing JSON will * cause the column to be created (not modified) as a JSON column. * However it might also trigger exceptions if this means the DB attempts to * convert a non-json column to a JSON column. * * Feature #2: * OODBBean::convertArraysToJSON * * Toggles array to JSON conversion. If set to TRUE any array * set to a bean property that's not a list will be turned into * a JSON string. Used together with AQueryWriter::useJSONColumns this * extends the data type support for JSON columns. * * So invoking this method is the same as: * * AQueryWriter::useJSONColumns( $flag ); * OODBBean::convertArraysToJSON( $flag ); * * Unlike the methods above, that return the previous state, this * method does not return anything (void). * * @param boolean $flag feature flag (either TRUE or FALSE) * * @return void */ public static function useJSONFeatures( $flag ) { AQueryWriter::useJSONColumns( $flag ); OODBBean::convertArraysToJSON( $flag ); } /** * Dynamically extends the facade with a plugin. * Using this method you can register your plugin with the facade and then * use the plugin by invoking the name specified plugin name as a method on * the facade. * * Usage: * * * R::ext( 'makeTea', function() { ... } ); * * * Now you can use your makeTea plugin like this: * * * R::makeTea(); * * * @param string $pluginName name of the method to call the plugin * @param callable $callable a PHP callable * * @return void */ public static function ext( $pluginName, $callable ) { if ( !ctype_alnum( $pluginName ) ) { throw new RedException( 'Plugin name may only contain alphanumeric characters.' ); } self::$plugins[$pluginName] = $callable; } /** * Call static for use with dynamic plugins. This magic method will * intercept static calls and route them to the specified plugin. * * @param string $pluginName name of the plugin * @param array $params list of arguments to pass to plugin method * * @return mixed */ public static function __callStatic( $pluginName, $params ) { if ( !ctype_alnum( $pluginName) ) { throw new RedException( 'Plugin name may only contain alphanumeric characters.' ); } if ( !isset( self::$plugins[$pluginName] ) ) { throw new RedException( 'Plugin \''.$pluginName.'\' does not exist, add this plugin using: R::ext(\''.$pluginName.'\')' ); } return call_user_func_array( self::$plugins[$pluginName], $params ); } } } namespace RedBeanPHP { use RedBeanPHP\ToolBox as ToolBox; use RedBeanPHP\AssociationManager as AssociationManager; use RedBeanPHP\OODB as OODB; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; /** * Duplication Manager * The Duplication Manager creates deep copies from beans, this means * it can duplicate an entire bean hierarchy. You can use this feature to * implement versioning for instance. Because duplication and exporting are * closely related this class is also used to export beans recursively * (i.e. we make a duplicate and then convert to array). This class allows * you to tune the duplication process by specifying filters determining * which relations to take into account and by specifying tables * (in which case no reflective queries have to be issued thus improving * performance). This class also hosts the Camelfy function used to * reformat the keys of an array, this method is publicly available and * used internally by exportAll(). * * @file RedBeanPHP/DuplicationManager.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. */ class DuplicationManager { /** * @var ToolBox */ protected $toolbox; /** * @var AssociationManager */ protected $associationManager; /** * @var OODB */ protected $redbean; /** * @var array */ protected $tables = array(); /** * @var array */ protected $columns = array(); /** * @var array */ protected $filters = array(); /** * @var array */ protected $cacheTables = FALSE; /** * Copies the shared beans in a bean, i.e. all the sharedBean-lists. * * @param OODBBean $copy target bean to copy lists to * @param string $shared name of the shared list * @param array $beans array with shared beans to copy * * @return void */ private function copySharedBeans( OODBBean $copy, $shared, $beans ) { $copy->$shared = array(); foreach ( $beans as $subBean ) { array_push( $copy->$shared, $subBean ); } } /** * Copies the own beans in a bean, i.e. all the ownBean-lists. * Each bean in the own-list belongs exclusively to its owner so * we need to invoke the duplicate method again to duplicate each bean here. * * @param OODBBean $copy target bean to copy lists to * @param string $owned name of the own list * @param array $beans array with shared beans to copy * @param array $trail array with former beans to detect recursion * @param boolean $preserveIDs TRUE means preserve IDs, for export only * * @return void */ private function copyOwnBeans( OODBBean $copy, $owned, $beans, $trail, $preserveIDs ) { $copy->$owned = array(); foreach ( $beans as $subBean ) { array_push( $copy->$owned, $this->duplicate( $subBean, $trail, $preserveIDs ) ); } } /** * Creates a copy of bean $bean and copies all primitive properties (not lists) * and the parents beans to the newly created bean. Also sets the ID of the bean * to 0. * * @param OODBBean $bean bean to copy * * @return OODBBean */ private function createCopy( OODBBean $bean ) { $type = $bean->getMeta( 'type' ); $copy = $this->redbean->dispense( $type ); $copy->setMeta( 'sys.dup-from-id', $bean->id ); $copy->setMeta( 'sys.old-id', $bean->id ); $copy->importFrom( $bean ); $copy->id = 0; return $copy; } /** * Generates a key from the bean type and its ID and determines if the bean * occurs in the trail, if not the bean will be added to the trail. * Returns TRUE if the bean occurs in the trail and FALSE otherwise. * * @param array $trail list of former beans * @param OODBBean $bean currently selected bean * * @return boolean */ private function inTrailOrAdd( &$trail, OODBBean $bean ) { $type = $bean->getMeta( 'type' ); $key = $type . $bean->getID(); if ( isset( $trail[$key] ) ) { return TRUE; } $trail[$key] = $bean; return FALSE; } /** * Given the type name of a bean this method returns the canonical names * of the own-list and the shared-list properties respectively. * Returns a list with two elements: name of the own-list, and name * of the shared list. * * @param string $typeName bean type name * * @return array */ private function getListNames( $typeName ) { $owned = 'own' . ucfirst( $typeName ); $shared = 'shared' . ucfirst( $typeName ); return array( $owned, $shared ); } /** * Determines whether the bean has an own list based on * schema inspection from realtime schema or cache. * * @param string $type bean type to get list for * @param string $target type of list you want to detect * * @return boolean */ protected function hasOwnList( $type, $target ) { return isset( $this->columns[$target][$type . '_id'] ); } /** * Determines whether the bea has a shared list based on * schema inspection from realtime schema or cache. * * @param string $type bean type to get list for * @param string $target type of list you are looking for * * @return boolean */ protected function hasSharedList( $type, $target ) { return in_array( AQueryWriter::getAssocTableFormat( array( $type, $target ) ), $this->tables ); } /** * @see DuplicationManager::dup * * @param OODBBean $bean bean to be copied * @param array $trail trail to prevent infinite loops * @param boolean $preserveIDs preserve IDs * * @return OODBBean */ protected function duplicate( OODBBean $bean, $trail = array(), $preserveIDs = FALSE ) { if ( $this->inTrailOrAdd( $trail, $bean ) ) return $bean; $type = $bean->getMeta( 'type' ); $copy = $this->createCopy( $bean ); foreach ( $this->tables as $table ) { if ( !empty( $this->filters ) ) { if ( !in_array( $table, $this->filters ) ) continue; } list( $owned, $shared ) = $this->getListNames( $table ); if ( $this->hasSharedList( $type, $table ) ) { if ( $beans = $bean->$shared ) { $this->copySharedBeans( $copy, $shared, $beans ); } } elseif ( $this->hasOwnList( $type, $table ) ) { if ( $beans = $bean->$owned ) { $this->copyOwnBeans( $copy, $owned, $beans, $trail, $preserveIDs ); } $copy->setMeta( 'sys.shadow.' . $owned, NULL ); } $copy->setMeta( 'sys.shadow.' . $shared, NULL ); } $copy->id = ( $preserveIDs ) ? $bean->id : $copy->id; return $copy; } /** * Constructor, * creates a new instance of DupManager. * * @param ToolBox $toolbox */ public function __construct( ToolBox $toolbox ) { $this->toolbox = $toolbox; $this->redbean = $toolbox->getRedBean(); $this->associationManager = $this->redbean->getAssociationManager(); } /** * Recursively turns the keys of an array into * camelCase. * * @param array $array array to camelize * @param boolean $dolphinMode whether you want the exception for IDs. * * @return array */ public function camelfy( $array, $dolphinMode = false ) { $newArray = array(); foreach( $array as $key => $element ) { $newKey = preg_replace_callback( '/_(\w)/', function( &$matches ){ return strtoupper( $matches[1] ); }, $key); if ( $dolphinMode ) { $newKey = preg_replace( '/(\w)Id$/', '$1ID', $newKey ); } $newArray[$newKey] = ( is_array($element) ) ? $this->camelfy( $element, $dolphinMode ) : $element; } return $newArray; } /** * For better performance you can pass the tables in an array to this method. * If the tables are available the duplication manager will not query them so * this might be beneficial for performance. * * This method allows two array formats: * * * array( TABLE1, TABLE2 ... ) * * * or * * * array( TABLE1 => array( COLUMN1, COLUMN2 ... ) ... ) * * * @param array $tables a table cache array * * @return void */ public function setTables( $tables ) { foreach ( $tables as $key => $value ) { if ( is_numeric( $key ) ) { $this->tables[] = $value; } else { $this->tables[] = $key; $this->columns[$key] = $value; } } $this->cacheTables = TRUE; } /** * Returns a schema array for cache. * You can use the return value of this method as a cache, * store it in RAM or on disk and pass it to setTables later. * * @return array */ public function getSchema() { return $this->columns; } /** * Indicates whether you want the duplication manager to cache the database schema. * If this flag is set to TRUE the duplication manager will query the database schema * only once. Otherwise the duplicationmanager will, by default, query the schema * every time a duplication action is performed (dup()). * * @param boolean $yesNo TRUE to use caching, FALSE otherwise */ public function setCacheTables( $yesNo ) { $this->cacheTables = $yesNo; } /** * A filter array is an array with table names. * By setting a table filter you can make the duplication manager only take into account * certain bean types. Other bean types will be ignored when exporting or making a * deep copy. If no filters are set all types will be taking into account, this is * the default behavior. * * @param array $filters list of tables to be filtered * * @return void */ public function setFilters( $filters ) { if ( !is_array( $filters ) ) { $filters = array( $filters ); } $this->filters = $filters; } /** * Makes a copy of a bean. This method makes a deep copy * of the bean.The copy will have the following features. * - All beans in own-lists will be duplicated as well * - All references to shared beans will be copied but not the shared beans themselves * - All references to parent objects (_id fields) will be copied but not the parents themselves * In most cases this is the desired scenario for copying beans. * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found * (i.e. one that already has been processed) the ID of the bean will be returned. * This should not happen though. * * Note: * This function does a reflectional database query so it may be slow. * * Note: * this function actually passes the arguments to a protected function called * duplicate() that does all the work. This method takes care of creating a clone * of the bean to avoid the bean getting tainted (triggering saving when storing it). * * @param OODBBean $bean bean to be copied * @param array $trail for internal usage, pass array() * @param boolean $preserveIDs for internal usage * * @return OODBBean */ public function dup( OODBBean $bean, $trail = array(), $preserveIDs = FALSE ) { if ( !count( $this->tables ) ) { $this->tables = $this->toolbox->getWriter()->getTables(); } if ( !count( $this->columns ) ) { foreach ( $this->tables as $table ) { $this->columns[$table] = $this->toolbox->getWriter()->getColumns( $table ); } } $rs = $this->duplicate( ( clone $bean ), $trail, $preserveIDs ); if ( !$this->cacheTables ) { $this->tables = array(); $this->columns = array(); } return $rs; } /** * Exports a collection of beans recursively. * This method will export an array of beans in the first argument to a * set of arrays. This can be used to send JSON or XML representations * of bean hierarchies to the client. * * For every bean in the array this method will export: * * - contents of the bean * - all own bean lists (recursively) * - all shared beans (but not THEIR own lists) * * If the second parameter is set to TRUE the parents of the beans in the * array will be exported as well (but not THEIR parents). * * The third parameter can be used to provide a white-list array * for filtering. This is an array of strings representing type names, * only the type names in the filter list will be exported. * * The fourth parameter can be used to change the keys of the resulting * export arrays. The default mode is 'snake case' but this leaves the * keys as-is, because 'snake' is the default case style used by * RedBeanPHP in the database. You can set this to 'camel' for * camel cased keys or 'dolphin' (same as camelcase but id will be * converted to ID instead of Id). * * @param array|OODBBean $beans beans to be exported * @param boolean $parents also export parents * @param array $filters only these types (whitelist) * @param string $caseStyle case style identifier * * @return array */ public function exportAll( $beans, $parents = FALSE, $filters = array(), $caseStyle = 'snake') { $array = array(); if ( !is_array( $beans ) ) { $beans = array( $beans ); } foreach ( $beans as $bean ) { $this->setFilters( $filters ); $duplicate = $this->dup( $bean, array(), TRUE ); $array[] = $duplicate->export( FALSE, $parents, FALSE, $filters ); } if ( $caseStyle === 'camel' ) $array = $this->camelfy( $array ); if ( $caseStyle === 'dolphin' ) $array = $this->camelfy( $array, true ); return $array; } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\RedException as RedException; /** * Array Tool Helper * * This code was originally part of the facade, however it has * been decided to remove unique features to service classes like * this to make them available to developers not using the facade class. * * This is a helper or service class containing frequently used * array functions for dealing with SQL queries. * * @file RedBeanPHP/Util/ArrayTool.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. */ class ArrayTool { /** * Generates question mark slots for an array of values. * * @param array $array array to generate question mark slots for * @param string $template template to use * * @return string */ public static function genSlots( $array, $template = NULL ) { $str = count( $array ) ? implode( ',', array_fill( 0, count( $array ), '?' ) ) : ''; return ( is_null( $template ) || $str === '' ) ? $str : sprintf( $template, $str ); } /** * Flattens a multi dimensional bindings array for use with genSlots(). * * @param array $array array to flatten * @param array $result result array parameter (for recursion) * * @return array */ public static function flat( $array, $result = array() ) { foreach( $array as $value ) { if ( is_array( $value ) ) $result = self::flat( $value, $result ); else $result[] = $value; } return $result; } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; use RedBeanPHP\RedException as RedException; /** * Dispense Helper * * A helper class containing a dispense utility. * * @file RedBeanPHP/Util/DispenseHelper.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. */ class DispenseHelper { /** * Checks whether the bean type conforms to the RedbeanPHP * naming policy. This method will throw an exception if the * type does not conform to the RedBeanPHP database column naming * policy. * * @param string $type type of bean * * @return void */ public static function checkType( $type ) { if ( !preg_match( '/^[a-z0-9]+$/', $type ) ) { throw new RedException( 'Invalid type: ' . $type ); } } /** * Dispenses a new RedBean OODB Bean for use with * the rest of the methods. * * @param OODB $oodb OODB * @param string|array $typeOrBeanArray type or bean array to import * @param integer $num number of beans to dispense * @param boolean $alwaysReturnArray if TRUE always returns the result as an array * * @return array|OODBBean */ public static function dispense( OODB $oodb, $typeOrBeanArray, $num = 1, $alwaysReturnArray = FALSE ) { if ( is_array($typeOrBeanArray) ) { if ( !isset( $typeOrBeanArray['_type'] ) ) { $list = array(); foreach( $typeOrBeanArray as $beanArray ) { if ( !( is_array( $beanArray ) && isset( $beanArray['_type'] ) ) ) { throw new RedException( 'Invalid Array Bean' ); } } foreach( $typeOrBeanArray as $beanArray ) $list[] = self::dispense( $oodb, $beanArray ); return $list; } $import = $typeOrBeanArray; $type = $import['_type']; unset( $import['_type'] ); } else { $type = $typeOrBeanArray; } self::checkType( $type ); $beanOrBeans = $oodb->dispense( $type, $num, $alwaysReturnArray ); if ( isset( $import ) ) { $beanOrBeans->import( $import ); } return $beanOrBeans; } /** * Takes a comma separated list of bean types * and dispenses these beans. For each type in the list * you can specify the number of beans to be dispensed. * * Usage: * * * list( $book, $page, $text ) = R::dispenseAll( 'book,page,text' ); * * * This will dispense a book, a page and a text. This way you can * quickly dispense beans of various types in just one line of code. * * Usage: * * * list($book, $pages) = R::dispenseAll('book,page*100'); * * * This returns an array with a book bean and then another array * containing 100 page beans. * * @param OODB $oodb OODB * @param string $order a description of the desired dispense order using the syntax above * @param boolean $onlyArrays return only arrays even if amount < 2 * * @return array */ public static function dispenseAll( OODB $oodb, $order, $onlyArrays = FALSE ) { $list = array(); foreach( explode( ',', $order ) as $order ) { if ( strpos( $order, '*' ) !== FALSE ) { list( $type, $amount ) = explode( '*', $order ); } else { $type = $order; $amount = 1; } $list[] = self::dispense( $oodb, $type, $amount, $onlyArrays ); } return $list; } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; use RedBeanPHP\OODBBean as OODBBean; /** * Dump helper * * This code was originally part of the facade, however it has * been decided to remove unique features to service classes like * this to make them available to developers not using the facade class. * * Dumps the contents of a bean in an array for * debugging purposes. * * @file RedBeanPHP/Util/Dump.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. */ class Dump { /** * Simple but effective debug function. * Given a one or more beans this method will * return an array containing first part of the string * representation of each item in the array. * * @param OODBBean|array $data either a bean or an array of beans * * @return array */ public static function dump( $data ) { $array = array(); if ( $data instanceof OODBBean ) { $str = strval( $data ); if (strlen($str) > 35) { $beanStr = substr( $str, 0, 35 ).'... '; } else { $beanStr = $str; } return $beanStr; } if ( is_array( $data ) ) { foreach( $data as $key => $item ) { $array[$key] = self::dump( $item ); } } return $array; } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; /** * Multi Bean Loader Helper * * This code was originally part of the facade, however it has * been decided to remove unique features to service classes like * this to make them available to developers not using the facade class. * * This helper class offers limited support for one-to-one * relations by providing a service to load a set of beans * with differnt types and a common ID. * * @file RedBeanPHP/Util/MultiLoader.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. */ class MultiLoader { /** * Loads multiple types of beans with the same ID. * This might look like a strange method, however it can be useful * for loading a one-to-one relation. * * @param OODB $oodb OODB object * @param string|array $types the set of types to load at once * @param mixed $id the common ID * * @return OODBBean */ public static function load( OODB $oodb, $types, $id ) { if ( is_string( $types ) ) { $types = explode( ',', $types ); } if ( !is_array( $types ) ) { return array(); } foreach ( $types as $k => $typeItem ) { $types[$k] = $oodb->load( $typeItem, $id ); } return $types; } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\RedException as RedException; use RedBeanPHP\Adapter as Adapter; /** * Transaction Helper * * This code was originally part of the facade, however it has * been decided to remove unique features to service classes like * this to make them available to developers not using the facade class. * * Database transaction helper. This is a convenience class * to perform a callback in a database transaction. This class * contains a method to wrap your callback in a transaction. * * @file RedBeanPHP/Util/Transaction.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. */ class Transaction { /** * Wraps a transaction around a closure or string callback. * If an Exception is thrown inside, the operation is automatically rolled back. * If no Exception happens, it commits automatically. * It also supports (simulated) nested transactions (that is useful when * you have many methods that needs transactions but are unaware of * each other). * * Example: * * * $from = 1; * $to = 2; * $amount = 300; * * R::transaction(function() use($from, $to, $amount) * { * $accountFrom = R::load('account', $from); * $accountTo = R::load('account', $to); * $accountFrom->money -= $amount; * $accountTo->money += $amount; * R::store($accountFrom); * R::store($accountTo); * }); * * * @param Adapter $adapter Database Adapter providing transaction mechanisms. * @param callable $callback Closure (or other callable) with the transaction logic * * @return mixed */ public static function transaction( Adapter $adapter, $callback ) { if ( !is_callable( $callback ) ) { throw new RedException( 'R::transaction needs a valid callback.' ); } static $depth = 0; $result = null; try { if ( $depth == 0 ) { $adapter->startTransaction(); } $depth++; $result = call_user_func( $callback ); //maintain 5.2 compatibility $depth--; if ( $depth == 0 ) { $adapter->commit(); } } catch ( \Exception $exception ) { $depth--; if ( $depth == 0 ) { $adapter->rollback(); } throw $exception; } return $result; } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\ToolBox as ToolBox; /** * Quick Export Utility * * The Quick Export Utility Class provides functionality to easily * expose the result of SQL queries as well-known formats like CSV. * * @file RedBeanPHP/Util/QuickExporft.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. */ class QuickExport { /** * @var Finder */ protected $toolbox; /** * Constructor. * The Quick Export requires a toolbox. * * @param ToolBox $toolbox */ public function __construct( ToolBox $toolbox ) { $this->toolbox = $toolbox; } /** * Exposes the result of the specified SQL query as a CSV file. * Usage: * * R::csv( 'SELECT * `name`, * population * FROM city * WHERE region = :region ', * array( ':region' => 'Denmark' ), * array( 'city', 'population' ), * '/tmp/cities.csv' * ); * * The command above will select all cities in Denmark * and create a CSV with columns 'city' and 'population' and * populate the cells under these column headers with the * names of the cities and the population numbers respectively. * * @param string $sql SQL query to expose result of * @param array $bindings parameter bindings * @param array $columns column headers for CSV file * @param string $path path to save CSV file to * @param boolean $output TRUE to output CSV directly using readfile * @param array $options delimiter, quote and escape character respectively * * @return void */ public function csv( $sql = '', $bindings = array(), $columns = NULL, $path = '/tmp/redexport_%s.csv', $output = true, $options = array(',','"','\\') ) { list( $delimiter, $enclosure, $escapeChar ) = $options; $path = sprintf( $path, date('Ymd_his') ); $handle = fopen( $path, 'w' ); if ($columns) fputcsv($handle, $columns, $delimiter, $enclosure, $escapeChar ); $cursor = $this->toolbox->getDatabaseAdapter()->getCursor( $sql, $bindings ); while( $row = $cursor->getNextItem() ) { fputcsv($handle, $row, $delimiter, $enclosure, $escapeChar ); } fclose($handle); if ( $output ) { $file = basename($path); header("Pragma: public"); header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); header("Cache-Control: private",false); header("Content-Type: text/csv"); header("Content-Disposition: attachment; filename={$file}" ); header("Content-Transfer-Encoding: binary"); readfile( $path ); @unlink( $path ); exit; } } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\ToolBox as ToolBox; use RedBeanPHP\Finder; /** * MatchUp Utility * * Tired of creating login systems and password-forget systems? * MatchUp is an ORM-translation of these kind of problems. * A matchUp is a match-and-update combination in terms of beans. * Typically login related problems are all about a match and * a conditional update. * * @file RedBeanPHP/Util/MatchUp.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. */ class MatchUp { /** * @var Toolbox */ protected $toolbox; /** * Constructor. * The MatchUp class requires a toolbox * * @param ToolBox $toolbox */ public function __construct( ToolBox $toolbox ) { $this->toolbox = $toolbox; } /** * MatchUp is a powerful productivity boosting method that can replace simple control * scripts with a single RedBeanPHP command. Typically, matchUp() is used to * replace login scripts, token generation scripts and password reset scripts. * The MatchUp method takes a bean type, an SQL query snippet (starting at the WHERE clause), * SQL bindings, a pair of task arrays and a bean reference. * * If the first 3 parameters match a bean, the first task list will be considered, * otherwise the second one will be considered. On consideration, each task list, * an array of keys and values will be executed. Every key in the task list should * correspond to a bean property while every value can either be an expression to * be evaluated or a closure (PHP 5.3+). After applying the task list to the bean * it will be stored. If no bean has been found, a new bean will be dispensed. * * This method will return TRUE if the bean was found and FALSE if not AND * there was a NOT-FOUND task list. If no bean was found AND there was also * no second task list, NULL will be returned. * * @param string $type type of bean you're looking for * @param string $sql SQL snippet (starting at the WHERE clause, omit WHERE-keyword) * @param array $bindings array of parameter bindings for SQL snippet * @param array $onFoundDo task list to be considered on finding the bean * @param array $onNotFoundDo task list to be considered on NOT finding the bean * @param OODBBean &$bean reference to obtain the found bean * * @return mixed */ public function matchUp( $type, $sql, $bindings = array(), $onFoundDo = NULL, $onNotFoundDo = NULL, &$bean = NULL ) { $finder = new Finder( $this->toolbox ); $oodb = $this->toolbox->getRedBean(); $bean = $finder->findOne( $type, $sql, $bindings ); if ( $bean && $onFoundDo ) { foreach( $onFoundDo as $property => $value ) { if ( function_exists('is_callable') && is_callable( $value ) ) { $bean[$property] = call_user_func_array( $value, array( $bean ) ); } else { $bean[$property] = $value; } } $oodb->store( $bean ); return TRUE; } if ( $onNotFoundDo ) { $bean = $oodb->dispense( $type ); foreach( $onNotFoundDo as $property => $value ) { if ( function_exists('is_callable') && is_callable( $value ) ) { $bean[$property] = call_user_func_array( $value, array( $bean ) ); } else { $bean[$property] = $value; } } $oodb->store( $bean ); return FALSE; } return NULL; } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\ToolBox as ToolBox; use RedBeanPHP\Finder; /** * Look Utility * * The Look Utility class provides an easy way to generate * tables and selects (pulldowns) from the database. * * @file RedBeanPHP/Util/Look.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. */ class Look { /** * @var Toolbox */ protected $toolbox; /** * Constructor. * The MatchUp class requires a toolbox * * @param ToolBox $toolbox */ public function __construct( ToolBox $toolbox ) { $this->toolbox = $toolbox; } /** * Takes an full SQL query with optional bindings, a series of keys, a template * and optionally a filter function and glue and assembles a view from all this. * This is the fastest way from SQL to view. Typically this function is used to * generate pulldown (select tag) menus with options queried from the database. * * @param string $sql query to execute * @param array $bindings parameters to bind to slots mentioned in query or an empty array * @param array $keys names in result collection to map to template * @param string $template HTML template to fill with values associated with keys, use printf notation (i.e. %s) * @param callable $filter function to pass values through (for translation for instance) * @param string $glue optional glue to use when joining resulting strings * * @return string */ public function look( $sql, $bindings = array(), $keys = array( 'selected', 'id', 'name' ), $template = '', $filter = 'trim', $glue = '' ) { $adapter = $this->toolbox->getDatabaseAdapter(); $lines = array(); $rows = $adapter->get( $sql, $bindings ); foreach( $rows as $row ) { $values = array(); foreach( $keys as $key ) { if (!empty($filter)) { $values[] = call_user_func_array( $filter, array( $row[$key] ) ); } else { $values[] = $row[$key]; } } $lines[] = vsprintf( $template, $values ); } $string = implode( $glue, $lines ); return $string; } } } namespace RedBeanPHP\Util { use RedBeanPHP\OODB as OODB; use RedBeanPHP\OODBBean as OODBBean; use RedBeanPHP\ToolBox as ToolBox; use RedBeanPHP\Finder; /** * Diff Utility * * The Look Utility class provides an easy way to generate * tables and selects (pulldowns) from the database. * * @file RedBeanPHP/Util/Diff.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. */ class Diff { /** * @var Toolbox */ protected $toolbox; /** * Constructor. * The MatchUp class requires a toolbox * * @param ToolBox $toolbox */ public function __construct( ToolBox $toolbox ) { $this->toolbox = $toolbox; } /** * Calculates a diff between two beans (or arrays of beans). * The result of this method is an array describing the differences of the second bean compared to * the first, where the first bean is taken as reference. The array is keyed by type/property, id and property name, where * type/property is either the type (in case of the root bean) or the property of the parent bean where the type resides. * The diffs are mainly intended for logging, you cannot apply these diffs as patches to other beans. * However this functionality might be added in the future. * * The keys of the array can be formatted using the $format parameter. * A key will be composed of a path (1st), id (2nd) and property (3rd). * Using printf-style notation you can determine the exact format of the key. * The default format will look like: * * 'book.1.title' => array( , ) * * If you only want a simple diff of one bean and you don't care about ids, * you might pass a format like: '%1$s.%3$s' which gives: * * 'book.1.title' => array( , ) * * The filter parameter can be used to set filters, it should be an array * of property names that have to be skipped. By default this array is filled with * two strings: 'created' and 'modified'. * * @param OODBBean|array $beans reference beans * @param OODBBean|array $others beans to compare * @param array $filters names of properties of all beans to skip * @param string $format the format of the key, defaults to '%s.%s.%s' * @param string $type type/property of bean to use for key generation * * @return array */ public function diff( $beans, $others, $filters = array( 'created', 'modified' ), $format = '%s.%s.%s', $type = NULL ) { $diff = array(); if ( !is_array( $beans ) ) $beans = array( $beans ); if ( !is_array( $others ) ) $others = array( $others ); foreach( $beans as $bean ) { if ( !is_object( $bean ) ) continue; if ( !( $bean instanceof OODBBean ) ) continue; if ( $type == NULL ) $type = $bean->getMeta( 'type' ); foreach( $others as $other ) { if ( !is_object( $other ) ) continue; if ( !( $other instanceof OODBBean ) ) continue; if ( $other->id == $bean->id ) { foreach( $bean as $property => $value ) { if ( in_array( $property, $filters ) ) continue; $key = vsprintf( $format, array( $type, $bean->id, $property ) ); $compare = $other->{$property}; if ( !is_object( $value ) && !is_array( $value ) ) { if ( $value != $compare ) { $diff[$key] = array( $value, $compare ); } continue; } else { $diff = array_merge( $diff, $this->diff( $value, $compare, $filters, $format, $key ) ); continue; } } } } } return $diff; } } } namespace RedBeanPHP { /** * RedBean Plugin. * Marker interface for plugins. * Use this interface when defining new plugins, it's an * easy way for the rest of the application to recognize your * plugin. This plugin interface does not require you to * implement a specific API. * * @file RedBean/Plugin.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. */ interface Plugin { } ; } namespace { //make some classes available for backward compatibility class RedBean_SimpleModel extends \RedBeanPHP\SimpleModel {}; if (!class_exists('R')) { class R extends \RedBeanPHP\Facade{}; } /** * Support functions for RedBeanPHP. * Additional convenience shortcut functions for RedBeanPHP. * * @file RedBeanPHP/Functions.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. */ /** * Convenience function for ENUM short syntax in queries. * * Usage: * * * R::find( 'paint', ' color_id = ? ', [ EID('color:yellow') ] ); * * * If a function called EID() already exists you'll have to write this * wrapper yourself ;) * * @param string $enumName enum code as you would pass to R::enum() * * @return mixed */ if (!function_exists('EID')) { function EID($enumName) { return \RedBeanPHP\Facade::enum( $enumName )->id; } } /** * Prints the result of R::dump() to the screen using * print_r. * * @param mixed $data data to dump * * @return void */ if ( !function_exists( 'dmp' ) ) { function dmp( $list ) { print_r( \RedBeanPHP\Facade::dump( $list ) ); } } /** * Function alias for R::genSlots(). */ if ( !function_exists( 'genslots' ) ) { function genslots( $slots, $tpl = NULL ) { return \RedBeanPHP\Facade::genSlots( $slots, $tpl ); } } /** * Function alias for R::flat(). */ if ( !function_exists( 'array_flatten' ) ) { function array_flatten( $array ) { return \RedBeanPHP\Facade::flat( $array ); } } }