diff --git a/libs/Autoloader.php b/libs/Autoloader.php
new file mode 100644
index 0000000..e888553
--- /dev/null
+++ b/libs/Autoloader.php
@@ -0,0 +1,107 @@
+ 'Smarty.class.php', 'smartybc' => 'SmartyBC.class.php',);
+
+ /**
+ * Registers Smarty_Autoloader backward compatible to older installations.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not.
+ */
+ public static function registerBC($prepend = false)
+ {
+ /**
+ * register the class autoloader
+ */
+ if (!defined('SMARTY_SPL_AUTOLOAD')) {
+ define('SMARTY_SPL_AUTOLOAD', 0);
+ }
+ if (SMARTY_SPL_AUTOLOAD &&
+ set_include_path(get_include_path() . PATH_SEPARATOR . SMARTY_SYSPLUGINS_DIR) !== false
+ ) {
+ $registeredAutoLoadFunctions = spl_autoload_functions();
+ if (!isset($registeredAutoLoadFunctions[ 'spl_autoload' ])) {
+ spl_autoload_register();
+ }
+ } else {
+ self::register($prepend);
+ }
+ }
+
+ /**
+ * Registers Smarty_Autoloader as an SPL autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not.
+ */
+ public static function register($prepend = false)
+ {
+ self::$SMARTY_DIR = defined('SMARTY_DIR') ? SMARTY_DIR : dirname(__FILE__) . DIRECTORY_SEPARATOR;
+ self::$SMARTY_SYSPLUGINS_DIR = defined('SMARTY_SYSPLUGINS_DIR') ? SMARTY_SYSPLUGINS_DIR :
+ self::$SMARTY_DIR . 'sysplugins' . DIRECTORY_SEPARATOR;
+ if (version_compare(phpversion(), '5.3.0', '>=')) {
+ spl_autoload_register(array(__CLASS__, 'autoload'), true, $prepend);
+ } else {
+ spl_autoload_register(array(__CLASS__, 'autoload'));
+ }
+ }
+
+ /**
+ * Handles auto loading of classes.
+ *
+ * @param string $class A class name.
+ */
+ public static function autoload($class)
+ {
+ $_class = strtolower($class);
+ if (strpos($_class, 'smarty') !== 0) {
+ return;
+ }
+ $file = self::$SMARTY_SYSPLUGINS_DIR . $_class . '.php';
+ if (is_file($file)) {
+ include $file;
+ } else if (isset(self::$rootClasses[ $_class ])) {
+ $file = self::$SMARTY_DIR . self::$rootClasses[ $_class ];
+ if (is_file($file)) {
+ include $file;
+ }
+ }
+ return;
+ }
+}
diff --git a/libs/License-R.class.txt b/libs/License-R.class.txt
old mode 100644
new mode 100755
index 4a01aac..97e7c64
--- a/libs/License-R.class.txt
+++ b/libs/License-R.class.txt
@@ -30,7 +30,7 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-RedBeanPHP is Written by Gabor de Mooij (G.J.G.T de Mooij) Copyright (c) 2014.
+RedBeanPHP is Written by Gabor de Mooij (G.J.G.T de Mooij) Copyright (c) 2016.
GPLv2 LICENSE
diff --git a/libs/R.class.php b/libs/R.class.php
index a851d8d..5d03ddf 100644
--- a/libs/R.class.php
+++ b/libs/R.class.php
@@ -1,4 +1,4 @@
-mode === 0 ) {
+ $log = var_export( $argument, TRUE );
+ if ( $this->mode === self::C_LOGGER_ECHO ) {
echo $log;
} else {
$this->logs[] = $log;
}
} else {
- if ( $this->mode === 0 ) {
+ if ( $this->mode === self::C_LOGGER_ECHO ) {
echo $argument;
} else {
$this->logs[] = $argument;
}
}
- if ($this->mode === 0) echo " \n";
+ if ( $this->mode === self::C_LOGGER_ECHO ) echo " " . PHP_EOL;
}
}
-
+
/**
- * Returns the logs array.
- *
+ * Returns the internal log array.
+ * The internal log array is where all log messages are stored.
+ *
* @return array
*/
public function getLogs()
{
return $this->logs;
}
-
+
/**
- * Empties the logs array.
- *
+ * Clears the internal log array, removing all
+ * previously stored entries.
+ *
* @return self
*/
public function clear()
@@ -120,38 +126,42 @@ class RDefault implements Logger
$this->logs = array();
return $this;
}
-
+
/**
* Selects a logging mode.
- * Mode 0 means echoing all statements, while mode 1
- * means populating the logs array.
- *
- * @param integer $mode 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 !== 0 && $mode !== 1) {
- throw new RedException( 'Invalid mode selected for logger, use 1 or 0.' );
+ 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.
- *
- * @param string $needle needle
- *
+ * 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;
+ if ( strpos( $logEntry, $needle ) !== FALSE ) $found[] = $logEntry;
}
return $found;
}
@@ -163,37 +173,44 @@ namespace RedBeanPHP\Logger\RDefault {
use RedBeanPHP\Logger as Logger;
use RedBeanPHP\Logger\RDefault as RDefault;
use RedBeanPHP\RedException as RedException;
-use RedBeanPHP\RedException\Security as Security;
/**
* Debug logger.
* A special logger for debugging purposes.
+ * Provides debugging logging functions for RedBeanPHP.
*
- * @file RedBean/Logger/RDefault/Debug.php
- * @desc Debug Logger
+ * @file RedBeanPHP/Logger/RDefault/Debug.php
* @author Gabor de Mooij and the RedBeanPHP Community
* @license BSD/GPLv2
*
- * Provides a debugging logging functions for RedBeanPHP.
- *
+ * @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 $bindings the bindings to process (key-value pairs)
+ * @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 ) {
@@ -207,7 +224,7 @@ class Debug extends RDefault implements Logger
* Fills in a value of a binding and truncates the
* resulting string if necessary.
*
- * @param mixed $value
+ * @param mixed $value bound value
*
* @return string
*/
@@ -216,11 +233,11 @@ class Debug extends RDefault implements Logger
if ( is_null( $value ) ) $value = 'NULL';
$value = strval( $value );
- if ( strlen( $value ) > 20 ) {
- $value = substr( $value, 0, 20 ).'... ';
+ if ( strlen( $value ) > ( $this->strLen ) ) {
+ $value = substr( $value, 0, ( $this->strLen ) ).'... ';
}
- if ( !is_numeric( $value ) && $value !== 'NULL') {
+ if ( !\RedBeanPHP\QueryWriter\AQueryWriter::canBeTreatedAsInt( $value ) && $value !== 'NULL') {
$value = '\''.$value.'\'';
}
@@ -232,6 +249,9 @@ class Debug extends RDefault implements Logger
* 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
@@ -239,7 +259,27 @@ class Debug extends RDefault implements Logger
protected function output( $str )
{
$this->logs[] = $str;
- if ( !$this->mode ) echo $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 ' ';
+ }
+ }
}
/**
@@ -252,15 +292,19 @@ class Debug extends RDefault implements Logger
*/
protected function normalizeSlots( $sql )
{
- $i = 0;
$newSql = $sql;
- while($i < 20 && strpos($newSql, '?') !== FALSE ){
+ $i = 0;
+ while(strpos($newSql, '?') !== FALSE ){
$pos = strpos( $newSql, '?' );
$slot = ':slot'.$i;
$begin = substr( $newSql, 0, $pos );
$end = substr( $newSql, $pos+1 );
- $newSql = $begin . $slot . $end;
- $i++;
+ if (PHP_SAPI === 'cli') {
+ $newSql = "{$begin}\e[32m{$slot}\e[39m{$end}";
+ } else {
+ $newSql = "{$begin}$slot{$end}";
+ }
+ $i ++;
}
return $newSql;
}
@@ -318,33 +362,46 @@ class Debug extends RDefault implements Logger
$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
- *
- * @file RedBean/Driver.php
- * @desc Describes the API for database classes
- * @author Gabor de Mooij and the RedBeanPHP Community
- * @license BSD/GPLv2
- *
+ * 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 to be executed
+ * @param string $sql SQL query to execute
* @param array $bindings list of values to bind to SQL snippet
*
* @return array
@@ -354,7 +411,7 @@ interface Driver
/**
* Runs a query and fetches results as a column.
*
- * @param string $sql SQL Code to execute
+ * @param string $sql SQL query to execute
* @param array $bindings list of values to bind to SQL snippet
*
* @return array
@@ -364,31 +421,31 @@ interface Driver
/**
* Runs a query and returns results as a single cell.
*
- * @param string $sql SQL to execute
+ * @param string $sql SQL query to execute
* @param array $bindings list of values to bind to SQL snippet
*
* @return mixed
*/
- public function GetCell( $sql, $bindings = array() );
-
+ 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 to execute
+ * @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 to execute
+ * @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() );
@@ -403,7 +460,7 @@ interface Driver
* 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 Code to execute
+ * @param string $sql SQL query to execute
* @param array $bindings list of values to bind to SQL snippet
*
* @return array Affected Rows
@@ -426,6 +483,16 @@ interface Driver
*/
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
@@ -433,11 +500,12 @@ interface Driver
* passes on to the screen for inspection.
* This method has no return value.
*
- * @param boolean $trueFalse turn on/off
+ * @param boolean $tf TRUE = debug mode ON
+ * @param Logger $customLogger
*
* @return void
*/
- public function setDebugMode( $tf );
+ public function setDebugMode( $tf, $customLogger );
/**
* Starts a transaction.
@@ -459,6 +527,20 @@ interface Driver
* @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();
}
}
@@ -467,20 +549,24 @@ 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
+ * PDO Driver
+ * This Driver implements the RedBean Driver API.
+ * for RedBeanPHP. This is the standard / default database driver
+ * for RedBeanPHP.
*
- * @file RedBean/PDO.php
- * @desc \PDO Driver
+ * @file RedBeanPHP/PDO.php
* @author Gabor de Mooij and the RedBeanPHP Community, Desfrenes
* @license BSD/GPLv2
*
- * (c) copyright Desfrenes & Gabor de Mooij and the RedBeanPHP community
+ * @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.
*/
@@ -499,7 +585,7 @@ class RPDO implements Driver
/**
* @var boolean
*/
- protected $debug = FALSE;
+ protected $loggingEnabled = FALSE;
/**
* @var Logger
@@ -507,7 +593,7 @@ class RPDO implements Driver
protected $logger = NULL;
/**
- * @var\PDO
+ * @var PDO
*/
protected $pdo;
@@ -537,17 +623,37 @@ class RPDO implements Driver
protected $flagUseStringOnlyBinding = FALSE;
/**
- * @var string
+ * @var integer
*/
- protected $mysqlEncoding = '';
+ protected $queryCounter = 0;
/**
- * Binds parameters. This method binds parameters to a\PDOStatement for
+ * @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
+ * @param PDOStatement $statement PDO Statement instance
+ * @param array $bindings values that need to get bound to the statement
*
* @return void
*/
@@ -556,19 +662,19 @@ class RPDO implements Driver
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 ) && $value <= $this->max ) {
- $statement->bindParam( $key + 1, $value,\PDO::PARAM_INT );
+ $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 );
+ $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 ) && $value <= $this->max ) {
- $statement->bindParam( $key, $value,\PDO::PARAM_INT );
+ $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 );
+ $statement->bindParam( $key, $value, \PDO::PARAM_STR );
}
}
}
@@ -583,128 +689,179 @@ class RPDO implements Driver
*
* @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 void
- *
+ * @return mixed
* @throws SQL
*/
protected function runQuery( $sql, $bindings, $options = array() )
{
$this->connect();
-
- if ( $this->debug && $this->logger ) {
+ if ( $this->loggingEnabled && $this->logger ) {
$this->logger->log( $sql, $bindings );
}
-
try {
if ( strpos( 'pgsql', $this->dsn ) === 0 ) {
- $statement = $this->pdo->prepare( $sql, array(\PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT => TRUE ) );
+ 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->debug && $this->logger ) {
+ if ( $this->loggingEnabled && $this->logger ) {
$this->logger->log( 'resultset: ' . count( $this->resultArray ) . ' rows' );
}
} else {
$this->resultArray = array();
}
- } catch (\PDOException $e ) {
+ } 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->debug && $this->logger ) $this->logger->log( 'An error occurred: ' . $err );
-
- $exception = new SQL( $err, 0 );
+ 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 does not support proper 4 byte unicode but they
- * seem to have added it with version 5.5 under a different label: utf8mb4.
+ * 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 );
- $version = floatval( $this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION ) );
-
+ $driver = $this->pdo->getAttribute( \PDO::ATTR_DRIVER_NAME );
if ($driver === 'mysql') {
- $encoding = ($version >= 5.5) ? 'utf8mb4' : 'utf8';
- $this->pdo->setAttribute(\PDO::MYSQL_ATTR_INIT_COMMAND, 'SET NAMES '.$encoding ); //on every re-connect
- $this->pdo->exec(' SET NAMES '. $encoding); //also for current connection
- $this->mysqlEncoding = $encoding;
+ $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;
}
}
/**
- * Returns the best possible encoding for MySQL based on version data.
+ * Determine if a database supports a particular feature.
*
- * @return string
- */
- public function getMysqlEncoding()
+ * @param $db_cap identifier of database capability
+ *
+ * @return int|false Whether the database feature is supported, false otherwise.
+ **/
+ protected function hasCap( $db_cap )
{
- return $this->mysqlEncoding;
+ $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.
+ * 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
+ * @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 );
-
+ $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) {
+ 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.
*
@@ -715,14 +872,32 @@ class RPDO implements Driver
$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.
- *
- * @throws\PDOException
+ * UTF8 for the database and PDO-ERRMODE-EXCEPTION as well as
+ * PDO-FETCH-ASSOC.
*
* @return void
*/
@@ -732,26 +907,26 @@ class RPDO implements Driver
try {
$user = $this->connectInfo['user'];
$pass = $this->connectInfo['pass'];
-
- $this->pdo = new\PDO(
+ $this->pdo = new \PDO(
$this->dsn,
$user,
- $pass,
- array(\PDO::ATTR_ERRMODE =>\PDO::ERRMODE_EXCEPTION,
- \PDO::ATTR_DEFAULT_FETCH_MODE =>\PDO::FETCH_ASSOC,
- )
+ $pass
);
-
$this->setEncoding();
- $this->pdo->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, TRUE );
-
+ $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;
- } catch (\PDOException $exception ) {
+ /* 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() );
+ throw new \PDOException( 'Could not connect to database (' . $dbname . ').', $exception->getCode() );
}
}
@@ -776,7 +951,6 @@ class RPDO implements Driver
public function GetAll( $sql, $bindings = array() )
{
$this->runQuery( $sql, $bindings );
-
return $this->resultArray;
}
@@ -789,7 +963,6 @@ class RPDO implements Driver
'fetchStyle' => \PDO::FETCH_ASSOC
)
);
-
return $this->resultArray;
}
@@ -799,7 +972,6 @@ class RPDO implements Driver
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 ) {
@@ -811,16 +983,33 @@ class RPDO implements Driver
}
/**
- * @see Driver::GetCell
+ * @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() )
{
- $arr = $this->GetAll( $sql, $bindings );
-
- $row1 = array_shift( $arr );
- $col1 = array_shift( $row1 );
-
- return $col1;
+ return $this->GetOne( $sql, $bindings );
}
/**
@@ -829,7 +1018,6 @@ class RPDO implements Driver
public function GetRow( $sql, $bindings = array() )
{
$arr = $this->GetAll( $sql, $bindings );
-
return array_shift( $arr );
}
@@ -839,7 +1027,6 @@ class RPDO implements Driver
public function Execute( $sql, $bindings = array() )
{
$this->runQuery( $sql, $bindings );
-
return $this->affectedRows;
}
@@ -853,13 +1040,22 @@ class RPDO implements Driver
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;
}
@@ -868,21 +1064,18 @@ class RPDO implements Driver
* SQL to the screen together with some information about the
* results.
*
- * @param boolean $trueFalse turn on/off
- * @param Logger $logger logger instance
+ * @param boolean $trueFalse turn on/off
+ * @param Logger $logger logger instance
*
* @return void
*/
public function setDebugMode( $tf, $logger = NULL )
{
$this->connect();
-
- $this->debug = (bool) $tf;
-
- if ( $this->debug and !$logger ) {
+ $this->loggingEnabled = (bool) $tf;
+ if ( $this->loggingEnabled and !$logger ) {
$logger = new RDefault();
}
-
$this->setLogger( $logger );
}
@@ -891,10 +1084,13 @@ class RPDO implements Driver
* 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;
}
/**
@@ -914,7 +1110,6 @@ class RPDO implements Driver
public function StartTrans()
{
$this->connect();
-
$this->pdo->beginTransaction();
}
@@ -924,7 +1119,6 @@ class RPDO implements Driver
public function CommitTrans()
{
$this->connect();
-
$this->pdo->commit();
}
@@ -934,50 +1128,46 @@ class RPDO implements Driver
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.
+ * 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 $version 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.
+ * Returns the underlying PHP PDO instance.
*
- * @return\PDO
+ * @return PDO
*/
public function getPDO()
{
$this->connect();
-
return $this->pdo;
}
/**
- * Closes database connection by destructing\PDO.
+ * Closes database connection by destructing PDO.
*
* @return void
*/
@@ -988,7 +1178,7 @@ class RPDO implements Driver
}
/**
- * Returns TRUE if the current\PDO instance is connected.
+ * Returns TRUE if the current PDO instance is connected.
*
* @return boolean
*/
@@ -996,6 +1186,72 @@ class RPDO implements Driver
{
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;
+ }
}
}
@@ -1003,24 +1259,82 @@ namespace RedBeanPHP {
use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter;
use RedBeanPHP\BeanHelper as BeanHelper;
-use RedBeanPHP\RedException\Security as Security;
use RedBeanPHP\RedException as RedException;
-use RedBeanPHP\OODBBean as OODBBean;
+
+/* 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)
+ * OODBBean (Object Oriented DataBase Bean).
*
- * @file RedBean/OODBBean.php
- * @desc The Bean class used for passing information
+ * 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
+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).
@@ -1080,13 +1394,178 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
*/
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.
@@ -1096,7 +1575,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
*
* @param string $type the source type for the join
*
- * @return string $joinSql
+ * @return string
*/
private function parseJoin( $type )
{
@@ -1131,7 +1610,6 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
*/
private function getSharedList( $type, $redbean, $toolbox )
{
-
$writer = $toolbox->getWriter();
if ( $this->via ) {
@@ -1216,34 +1694,13 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
return $beans;
}
- /**
- * 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 ) {
- $bean->__info[ $property ] = $value;
- }
-
- 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 string $type type of the new bean
* @param BeanHelper $beanhelper bean helper to obtain a toolbox and a model
*
* @return void
@@ -1256,6 +1713,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$this->__info['sys.orig'] = array( 'id' => 0 );
$this->__info['tainted'] = TRUE;
$this->__info['changed'] = TRUE;
+ $this->__info['changelist'] = array();
$this->properties['id'] = 0;
}
@@ -1266,7 +1724,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
* 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
+ * @param BeanHelper $helper helper to use for this bean
*
* @return void
*/
@@ -1276,20 +1734,20 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
}
/**
- * Returns an\ArrayIterator so you can treat the bean like
+ * 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;
+ * $bean[$key] = $value;
*
* Note that not all PHP functions work with the array interface.
*
- * @return\ArrayIterator
+ * @return ArrayIterator
*/
public function getIterator()
{
- return new\ArrayIterator( $this->properties );
+ return new \ArrayIterator( $this->properties );
}
/**
@@ -1394,7 +1852,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
{
$myID = $this->properties['id'];
- $this->import( $otherBean->export() );
+ $this->import( $otherBean->export( FALSE, FALSE, TRUE ) );
$this->id = $myID;
@@ -1433,6 +1891,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$vn = array();
foreach ( $value as $i => $b ) {
+ if ( !( $b instanceof OODBBean ) ) continue;
$vn[] = $b->export( $meta, FALSE, FALSE, $filters );
$value = $vn;
}
@@ -1471,6 +1930,29 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
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.
*
@@ -1482,10 +1964,11 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
}
/**
- * Unsets a property. This method will load the property first using
- * __get.
+ * Unsets a property of a bean.
+ * Magic method, gets called implicitly when performing the unset() operation
+ * on a bean property.
*
- * @param string $property property
+ * @param string $property property to unset
*
* @return void
*/
@@ -1520,13 +2003,15 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
* 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
+ * @param string $sql SQL to be added to retrieval query.
+ * @param array $bindings array with parameters to bind to SQL snippet
*
* @return OODBBean
*/
@@ -1560,7 +2045,16 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
}
/**
- * When prefix for a list, this causes the list to reload.
+ * 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
*/
@@ -1590,16 +2084,22 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
* 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.
@@ -1645,13 +2145,15 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
/**
* Turns a camelcase property name into an underscored property name.
+ *
* Examples:
- * oneACLRoute -> one_acl_route
- * camelCase -> camel_case
+ *
+ * * oneACLRoute -> one_acl_route
+ * * camelCase -> camel_case
*
* Also caches the result to improve performance.
*
- * @param string $property
+ * @param string $property property to un-beautify
*
* @return string
*/
@@ -1662,9 +2164,9 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
if ( ctype_lower( $property ) ) return $property;
if (
- strpos( $property, 'own' ) === 0
- || strpos( $property, 'xown' ) === 0
- || strpos( $property, 'shared' ) === 0
+ ( 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 );
@@ -1678,8 +2180,6 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
return $beautifulColumns[$property];
}
-
-
/**
* Clears all modifiers.
*
@@ -1789,7 +2289,6 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
return $this->properties[$property];
}
-
list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox();
if ( isset( $this->$fieldLink ) ) {
@@ -1798,24 +2297,36 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
if ( isset( $this->__info["sys.parentcache.$property"] ) ) {
$bean = $this->__info["sys.parentcache.$property"];
} else {
- if ( $this->fetchType ) {
- $type = $this->fetchType;
+ if ( isset( self::$aliases[$property] ) ) {
+ $type = self::$aliases[$property];
+ } elseif ( $this->fetchType ) {
+ $type = $this->fetchType;
$this->fetchType = NULL;
} else {
$type = $property;
}
- $bean = $redbean->load( $type, $this->properties[$fieldLink] );
+ $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;
+ $this->withSql = '';
+ $this->withParams = array();
+ $this->aliasName = NULL;
+ $this->fetchType = NULL;
+ $this->noLoad = FALSE;
+ $this->all = FALSE;
+ $this->via = NULL;
return $this->properties[$property];
@@ -1829,7 +2340,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox );
}
- $this->properties[$property] = $beans;
+ $this->properties[$property] = $beans;
$this->__info["sys.shadow.$property"] = $beans;
$this->__info['tainted'] = TRUE;
@@ -1855,8 +2366,6 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
* @param mixed $value the value you want to assign
*
* @return void
- *
- * @throws Security
*/
public function __set( $property, $value )
{
@@ -1878,6 +2387,8 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
} 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));
@@ -1886,6 +2397,8 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$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) ) {
@@ -1910,6 +2423,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$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 ) {
@@ -1922,6 +2436,12 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
throw new RedException( 'Cannot cast to bean.' );
}
}
+
+ if ( $isFieldLink ){
+ unset( $this->properties[ $fieldName ]);
+ $this->properties[ $property ] = NULL;
+ }
+
if ( $value === FALSE ) {
$value = '0';
@@ -1960,17 +2480,21 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
/**
* Returns the value of a meta property. A meta property
- * contains extra information about the bean object that will not
- * get stored in the database. Meta information is used to instruct
- * RedBean as well as other systems how to deal with the bean.
- * For instance: $bean->setMeta("buildcommand.unique", array(
- * array("column1", "column2", "column3") ) );
- * Will add a UNIQUE constraint for the bean on columns: column1, column2 and
- * column 3.
- * To access a Meta property we use a dot separated notation.
+ * 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.
*
- * @param string $path path
+ * 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
@@ -1981,13 +2505,35 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
}
/**
- * Stores a value in the specified Meta information property. $value contains
- * the value you want to store in the Meta section of the bean and $path
- * specifies the dot separated path to the property. For instance "my.meta.property".
- * If "my" and "meta" do not exist they will be created automatically.
+ * 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
- * @param mixed $value value
+ * @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
*/
@@ -2003,7 +2549,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
* This is a convenience method to enable you to
* exchange meta information easily.
*
- * @param OODBBean $bean
+ * @param OODBBean $bean bean to copy meta data of
*
* @return OODBBean
*/
@@ -2016,6 +2562,20 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
/**
* 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
@@ -2024,6 +2584,12 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
*/
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 );
@@ -2034,6 +2600,37 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$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;
}
@@ -2052,10 +2649,21 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
*/
public function __toString()
{
- $string = $this->__call( '__toString', array() );
+ $string = $this->__call( '@__toString', array() );
if ( $string === NULL ) {
- return json_encode( $this->properties );
+ $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;
}
@@ -2149,7 +2757,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
* For polymorphic bean relations.
* Same as fetchAs but uses a column instead of a direct value.
*
- * @param string $column
+ * @param string $field field name to use for mapping
*
* @return OODBBean
*/
@@ -2165,10 +2773,12 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
*
* Can be used together with with, withCondition, alias and fetchAs.
*
- * @param string $property property
- * @param closure $function function
+ * @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 )
{
@@ -2193,7 +2803,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
if ( !is_array( $beans ) ) $beans = array( $beans );
foreach( $beans as $bean ) {
-
+ /** @var OODBBean $bean */
$function( $bean );
$bean->fetchType = $oldFetchType;
@@ -2208,7 +2818,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
}
/**
- * Implementation of\Countable interface. Makes it possible to use
+ * Implementation of Countable interface. Makes it possible to use
* count() function on a bean.
*
* @return integer
@@ -2314,7 +2924,12 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
* Returns TRUE if the value of a certain property of the bean has been changed and
* FALSE otherwise.
*
- * @param string $property name of the property you want the change-status of
+ * 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
*/
@@ -2324,6 +2939,45 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$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
@@ -2332,11 +2986,15 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
*
* 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
@@ -2344,8 +3002,8 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
* example #1. After preparing the linking bean, the bean is returned thus
* allowing the chained setter: ->song = $song.
*
- * @param string|OODBBean $type type of bean to dispense or the full bean
- * @param string|array $qualification JSON string or array (optional)
+ * @param string|OODBBean $typeOrBean type of bean to dispense or the full bean
+ * @param string|array $qualification JSON string or array (optional)
*
* @return OODBBean
*/
@@ -2377,6 +3035,21 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
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.
*
@@ -2431,13 +3104,15 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$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(), " $myFieldLink = :slot0 " . $this->withSql, $bindings );
+ $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(), " $myFieldLink = ? " . $this->withSql, $bindings );
+ $count = $this->beanHelper->getToolbox()->getWriter()->queryRecordCount( $type, array(), " {$joinSql} $myFieldLink = ? " . $this->withSql, $bindings );
}
}
@@ -2474,7 +3149,7 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
$count = 0;
if ( $this->getID() ) {
- $count = $redbean->getAssociationManager()->relatedCount( $this, $type, $this->withSql, $this->withParams, TRUE );
+ $count = $redbean->getAssociationManager()->relatedCount( $this, $type, $this->withSql, $this->withParams );
}
$this->clearModifiers();
@@ -2493,7 +3168,9 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
*
* Example:
*
+ *
* $quest->aggr( 'xownQuestTarget', 'target', 'quest' );
+ *
*
* Loads (in batch) and returns references to all
* quest beans residing in the $questTarget->target properties
@@ -2553,9 +3230,21 @@ class OODBBean implements\IteratorAggregate,\ArrayAccess,\Countable
);
}
-
+ /**
+ * 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 {
@@ -2565,11 +3254,11 @@ use RedBeanPHP\Observer as Observer;
* Observable
* Base class for Observables
*
- * @file RedBean/Observable.php
- * @description Part of the observer pattern in RedBean
+ * @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.
@@ -2588,7 +3277,7 @@ abstract class Observable { //bracket must be here - otherwise coverage software
* Second argument should be the object that wants to be notified in case
* the event occurs.
*
- * @param string $eventname event identifier
+ * @param string $eventname event identifier
* @param Observer $observer observer instance
*
* @return void
@@ -2630,33 +3319,34 @@ abstract class Observable { //bracket must be here - otherwise coverage software
}
}
}
-}
+}
namespace RedBeanPHP {
/**
- * Observer
+ * Observer.
+ *
* Interface for Observer object. Implementation of the
* observer pattern.
*
- * @file RedBean/Observer.php
- * @desc Part of the observer pattern in RedBean
- * @author Gabor de Mooijand the RedBeanPHP community
+ * @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
+ * 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
*
@@ -2664,27 +3354,30 @@ interface Observer
*/
public function onEvent( $eventname, $bean );
}
-}
+}
namespace RedBeanPHP {
/**
- * Adapter Interface
+ * Adapter Interface.
+ * Describes the API for a RedBeanPHP Database Adapter.
+ * This interface defines the API contract for
+ * a RedBeanPHP Database Adapter.
*
- * @file RedBean/Adapter.php
- * @desc Describes the API for a RedBean 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
{
-
/**
- * Returns the latest SQL statement
+ * Should returns a string containing the most recent SQL query
+ * that has been processed by the adapter.
*
* @return string
*/
@@ -2696,9 +3389,11 @@ interface Adapter
* observers to notify about the SQL execution; this to prevent
* infinite recursion when using observers.
*
- * @param string $sql SQL
- * @param array $bindings values
+ * @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 );
@@ -2708,8 +3403,8 @@ interface Adapter
* The values array can be used to bind values to the place holders in the
* SQL query.
*
- * @param string $sql SQL
- * @param array $bindings values
+ * @param string $sql string containing SQL code for database
+ * @param array $bindings array of values to bind to parameters in query string
*
* @return array
*/
@@ -2721,8 +3416,8 @@ interface Adapter
* The values array can be used to bind values to the place holders in the
* SQL query.
*
- * @param string $sql SQL
- * @param array $bindings values to bind
+ * @param string $sql string containing SQL code for database
+ * @param array $bindings array of values to bind to parameters in query string
*
* @return array
*/
@@ -2734,8 +3429,8 @@ interface Adapter
* The values array can be used to bind values to the place holders in the
* SQL query.
*
- * @param string $sql SQL
- * @param array $bindings values to bind
+ * @param string $sql string containing SQL code for database
+ * @param array $bindings array of values to bind to parameters in query string
*
* @return array
*/
@@ -2747,8 +3442,8 @@ interface Adapter
* The values array can be used to bind values to the place holders in the
* SQL query.
*
- * @param string $sql SQL
- * @param array $bindings values to bind
+ * @param string $sql string containing SQL code for database
+ * @param array $bindings array of values to bind to parameters in query string
*
* @return string
*/
@@ -2762,18 +3457,18 @@ interface Adapter
* The values array can be used to bind values to the place holders in the
* SQL query.
*
- * @param string $sql SQL
- * @param array $bindings values to bind
+ * @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 SQL
+ *
+ * @param string $sql Sstring containing SQL code for databaseQL
* @param array $bindings values to bind
*
* @return array
@@ -2795,13 +3490,23 @@ interface Adapter
*/
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 object
+ * @return mixed
*/
public function getDatabase();
@@ -2838,6 +3543,20 @@ interface Adapter
* @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 );
}
}
@@ -2850,20 +3569,24 @@ use RedBeanPHP\Driver as Driver;
/**
* DBAdapter (Database Adapter)
*
- * @file RedBean/Adapter/DBAdapter.php
- * @desc An adapter class to connect various database systems to RedBean
+ * 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
*
- * Database Adapter Class.
- *
+ * @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
*/
@@ -2889,7 +3612,17 @@ class DBAdapter extends Observable implements Adapter
}
/**
+ * 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()
{
@@ -2961,7 +3694,10 @@ class DBAdapter extends Observable implements Adapter
foreach ( $rows as $row ) {
if ( empty( $row ) ) continue;
- if ( count( $row ) > 1 ) {
+ if ( count( $row ) > 2 ) {
+ $key = array_shift( $row );
+ $value = $row;
+ } elseif ( count( $row ) > 1 ) {
$key = array_shift( $row );
$value = array_shift( $row );
} else {
@@ -2974,7 +3710,7 @@ class DBAdapter extends Observable implements Adapter
return $assoc;
}
-
+
/**
* @see Adapter::getAssocRow
*/
@@ -2995,13 +3731,15 @@ class DBAdapter extends Observable implements Adapter
if ( !$noSignal ) $this->signal( 'sql_exec', $this );
- $arr = $this->db->getCol( $sql, $bindings );
+ return $this->db->GetOne( $sql, $bindings );
+ }
- if ( $arr && is_array( $arr ) && isset( $arr[0] ) ) {
- return ( $arr[0] );
- }
-
- return NULL;
+ /**
+ * @see Adapter::getCursor
+ */
+ public function getCursor( $sql, $bindings = array() )
+ {
+ return $this->db->GetCursor( $sql, $bindings );
}
/**
@@ -3059,6 +3797,255 @@ class DBAdapter extends Observable implements Adapter
{
$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();
+ }
}
}
@@ -3066,22 +4053,25 @@ namespace RedBeanPHP {
/**
* QueryWriter
- * Interface for QueryWriters
+ * Interface for QueryWriters.
+ * Describes the API for a QueryWriter.
*
- * @file RedBean/QueryWriter.php
- * @desc 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
*
- * Notes:
- * - Whenever you see a parameter called $table or $type you should always
- * be aware of the fact that this argument contains a Bean Type string, not the
- * actual table name. These raw type names are passed to safeTable() to obtain the
- * actual name of the database table. Don't let the names confuse you $type/$table
- * refers to Bean Type, not physical database table names!
- * - This is the interface for FLUID database drivers. Drivers intended to support
- * just FROZEN mode should implement the IceWriter instead.
- *
+ * @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.
@@ -3091,7 +4081,7 @@ interface QueryWriter
/**
* SQL filter constants
*/
- const C_SQLFILTER_READ = 'r';
+ const C_SQLFILTER_READ = 'r';
const C_SQLFILTER_WRITE = 'w';
/**
@@ -3100,6 +4090,7 @@ interface QueryWriter
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
@@ -3126,9 +4117,12 @@ interface QueryWriter
* 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 $leftRight type of join (possible: 'LEFT', 'RIGHT' or 'INNER').
+ * @param string $joinType type of join (possible: 'LEFT', 'RIGHT' or 'INNER').
*
* @return string $joinSQLSnippet
*/
@@ -3146,9 +4140,9 @@ interface QueryWriter
*
* 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
+ * * 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.
@@ -3157,8 +4151,11 @@ interface QueryWriter
* For instance beginning a snippet with complex statements like JOIN or UNION
* will not work. This is too complex for use in a snippet.
*
- * @param string $sql SQL Snippet
- * @param integer $glue the GLUE type - how to glue (C_GLUE_WHERE or C_GLUE_AND)
+ * @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
*/
@@ -3168,6 +4165,9 @@ interface QueryWriter
* 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
@@ -3208,9 +4208,20 @@ interface QueryWriter
/**
* 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.
+ * 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).
*
- * @param string $value value
+ * 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
*/
@@ -3246,9 +4257,9 @@ interface QueryWriter
* 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
+ * @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
*/
@@ -3261,13 +4272,27 @@ interface QueryWriter
*
* @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 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.
@@ -3345,9 +4370,9 @@ interface QueryWriter
* 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
+ * @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
*/
@@ -3359,7 +4384,7 @@ interface QueryWriter
*
* @param string $type name of the table you want to query
* @param array $conditions criteria ( $column => array( $values ) )
- * @param string $sql additional SQL
+ * @param string $addSql additional SQL
* @param array $bindings bindings
*
* @return void
@@ -3377,16 +4402,21 @@ interface QueryWriter
*/
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 type
+ * @param string $type target bean type
* @param array $columnsPartOfIndex columns to include in index
*
* @return void
*/
- public function addUniqueIndex( $type, $columns );
+ public function addUniqueConstraint( $type, $columns );
/**
* This method will check whether the SQL state is in the list of specified states
@@ -3395,12 +4425,15 @@ interface QueryWriter
* 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
- * @param array $list list
+ * @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 );
+ public function sqlStateIn( $state, $list, $extraDriverDetails = array() );
/**
* This method will remove all beans of a certain type.
@@ -3412,16 +4445,6 @@ interface QueryWriter
*/
public function wipe( $type );
- /**
- * Given two types this method will add a foreign key constraint.
- *
- * @param string $sourceType source type
- * @param string $destType destination type
- *
- * @return void
- */
- public function addConstraintForTypes( $sourceType, $destType );
-
/**
* This method will add a foreign key from type and field to
* target type and target field.
@@ -3432,28 +4455,28 @@ interface QueryWriter
* 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 $field field that contains the foreign key value
- * @param string $targetField field where the fk points to
- * @param string $isDep whether target is dependent and should cascade on update/delete
+ * @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, $field, $targetField, $isDep = false );
+ 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 $column field to index
+ * @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, $column );
+ public function addIndex( $type, $name, $property );
/**
* Checks and filters a database structure element like a table of column
@@ -3480,11 +4503,15 @@ interface QueryWriter
* 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().
@@ -3492,24 +4519,42 @@ interface QueryWriter
* This method also accepts a single associative array as
* its first argument.
*
- * @param string|array $from
- * @param string $to (optional)
+ * @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( $from, $to = NULL );
+ 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.
+ * 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 );
}
}
@@ -3519,25 +4564,58 @@ 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;
/**
- * RedBean Abstract Query Writer
- *
- * @file RedBean/QueryWriter/AQueryWriter.php
- * @desc Query Writer (abstract class)
- * @author Gabor de Mooij and the RedBeanPHP Community
- * @license BSD/GPLv2
- *
+ * 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 { //bracket must be here - otherwise coverage software does not understand.
+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
*/
@@ -3564,30 +4642,90 @@ abstract class AQueryWriter { //bracket must be here - otherwise coverage softwa
protected $cache = array();
/**
- * @var array
+ * @var integer
*/
- public static $renames = array();
+ protected $maxCacheSizePerType = 20;
/**
- * @var array
+ * @var string
*/
- private static $sqlFilters = array();
-
- /**
- * @var boolean
- */
- private static $flagSQLFilterSafeMode = false;
-
- /**
- * @var boolean
- */
- private static $flagNarrowFieldMode = true;
+ 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.
*
@@ -3628,6 +4766,7 @@ abstract class AQueryWriter { //bracket must be here - otherwise coverage softwa
* This is a lowlevel method to set the SQL filter array.
* The format of this array is:
*
+ *
* array(
* '' => array(
* '
' => array(
@@ -3635,22 +4774,27 @@ abstract class AQueryWriter { //bracket must be here - otherwise coverage softwa
* )
* )
* )
+ *
*
* Example:
*
+ *
* array(
- * QueryWriter::C_SQLFILTER_READ => 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
+ * @param array list of filters to set
+ *
+ * @return void
*/
public static function setSQLFilters( $sqlFilters, $safeMode = false )
{
@@ -3671,76 +4815,6 @@ abstract class AQueryWriter { //bracket must be here - otherwise coverage softwa
return self::$sqlFilters;
}
- /**
- * 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 ), '?' ) );
- }
-
/**
* Returns a cache key for the cache values passed.
* This method returns a fingerprint string to be used as a key to store
@@ -3802,35 +4876,44 @@ abstract class AQueryWriter { //bracket must be here - otherwise coverage softwa
* 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.
- * You can only store one item under a cache tag. Why not use the cache tag as a key? Well
+ * 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
+ * @param string $key key to store values under
* @param array $values content to be stored
*
* @return void
*/
private function putResultInCache( $cacheTag, $key, $values )
{
- $this->cache[$cacheTag] = array(
- $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 SQL snippet
+ * @param string $addSql additional SQL snippet to append to result
*
* @return string
*/
@@ -3850,37 +4933,28 @@ abstract class AQueryWriter { //bracket must be here - otherwise coverage softwa
if ( !is_array( $values ) ) $values = array( $values );
- // If it's safe to skip bindings, do so...
- if ( ctype_digit( implode( '', $values ) ) ) {
- $sql .= implode( ',', $values ) . ' ) ';
+ if ( $paramTypeIsNum ) {
+ $sql .= implode( ',', array_fill( 0, count( $values ), '?' ) ) . ' ) ';
- // only numeric, cant do much harm
- $sqlConditions[] = $sql;
+ array_unshift($sqlConditions, $sql);
+
+ foreach ( $values as $k => $v ) {
+ $values[$k] = strval( $v );
+
+ array_unshift( $bindings, $v );
+ }
} else {
- if ( $paramTypeIsNum ) {
- $sql .= implode( ',', array_fill( 0, count( $values ), '?' ) ) . ' ) ';
+ $slots = array();
- 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;
+ foreach( $values as $k => $v ) {
+ $slot = ':slot'.$counter++;
+ $slots[] = $slot;
+ $bindings[$slot] = strval( $v );
}
+
+ $sql .= implode( ',', $slots ).' ) ';
+ $sqlConditions[] = $sql;
}
}
@@ -3923,6 +4997,167 @@ abstract class AQueryWriter { //bracket must be here - otherwise coverage softwa
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_