From 7a6d2ace00ae901dc25384af0ba5e7f84c6766a6 Mon Sep 17 00:00:00 2001 From: "color.diff=auto" Date: Wed, 24 Mar 2021 04:26:51 -0600 Subject: [PATCH] DB Dump Working --- .gitignore | 1 + _pdo.php | 89 + index.php | 48 +- src/D2Database.php | 74 + src/D2Functions.php | 2 +- src/D2Gems.php | 41 + src/D2SaveFile.php | 9 +- src/D2TxtParser.php | 15 +- src/D2UM.php | 4 +- src/header.php | 6 + txt/SetItems.txt | 7 + vendor/rb-sqlite.php | 17019 +++++++++++++++++++++++++++++++++++++++++ 12 files changed, 17280 insertions(+), 35 deletions(-) create mode 100644 .gitignore create mode 100644 _pdo.php create mode 100644 src/D2Database.php create mode 100644 src/D2Gems.php create mode 100644 vendor/rb-sqlite.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65eef93 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +db diff --git a/_pdo.php b/_pdo.php new file mode 100644 index 0000000..6321b6d --- /dev/null +++ b/_pdo.php @@ -0,0 +1,89 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); +} +function PDO_FetchOne($query, $params=null) +{ + global $PDO; + if (isset($params)) { + $stmt = $PDO->prepare($query); + $stmt->execute($params); + } else { + $stmt = $PDO->query($query); + } + $row = $stmt->fetch(PDO::FETCH_NUM); + if ($row) { + return $row[0]; + } else { + return false; + } +} +function PDO_FetchRow($query, $params=null) +{ + global $PDO; + if (isset($params)) { + $stmt = $PDO->prepare($query); + $stmt->execute($params); + } else { + $stmt = $PDO->query($query); + } + return $stmt->fetch(PDO::FETCH_ASSOC); +} +function PDO_FetchAll($query, $params=null) +{ + global $PDO; + if (isset($params)) { + $stmt = $PDO->prepare($query); + $stmt->execute($params); + } else { + $stmt = $PDO->query($query); + } + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} +function PDO_FetchAssoc($query, $params=null) +{ + global $PDO; + if (isset($params)) { + $stmt = $PDO->prepare($query); + $stmt->execute($params); + } else { + $stmt = $PDO->query($query); + } + $rows = $stmt->fetchAll(PDO::FETCH_NUM); + $assoc = array(); + foreach ($rows as $row) { + $assoc[$row[0]] = $row[1]; + } + return $assoc; +} +function PDO_Execute($query, $params=null) +{ + global $PDO; + if (isset($params)) { + $stmt = $PDO->prepare($query); + $stmt->execute($params); + return $stmt; + } else { + return $PDO->query($query); + } +} +function PDO_LastInsertId() +{ + global $PDO; + return $PDO->lastInsertId(); +} +function PDO_ErrorInfo() +{ + global $PDO; + return $PDO->errorInfo(); +} + +?> diff --git a/index.php b/index.php index d047357..246f3d2 100644 --- a/index.php +++ b/index.php @@ -20,10 +20,15 @@ along with D2UM. If not, see . */ -ini_set('display_errors', 1); +ini_set('display_errors', 0); +error_reporting(0); ini_set('log_errors', 1); define('FILTER_PROPERTIES_FILE', 'filterProperties.txt'); +define('DB_FILE', 'db'); + + +include "./_pdo.php"; $filename = 'd2im.conf'; @@ -39,6 +44,9 @@ if (file_exists($filename)) { // Misc functions require_once "./src/D2Functions.php"; + + // D2 Database + require_once "./src/D2Database.php"; // D2 Files Loader require_once './src/D2Files.php'; @@ -52,14 +60,18 @@ if (file_exists($filename)) { $parser = new D2TxtParser(); $armor = $parser->parseFile($files->getFile('Armor')); - $weapon = $parser->parseFile($files->getFile('Weapons')); + $gems = $parser->parseFile($files->getFile('Gems')); + $itypes = $parser->parseFile($files->getFile('ItemTypes')); + $misc = $parser->parseFile($files->getFile('Misc')); $prop = $parser->parseFile($files->getFile('Properties')); - $uni = $parser->parseFile($files->getFile('UniqueItems')); - $sets = $parser->parseFile($files->getFile('Sets')); $setitems = $parser->parseFile($files->getFile('SetItems')); - + $sets = $parser->parseFile($files->getFile('Sets')); + $uni = $parser->parseFile($files->getFile('UniqueItems')); + $weapon = $parser->parseFile($files->getFile('Weapons')); + $u = $files->getFile("UniqueItems"); $s = $files->getFile("SetItems"); + // If there's data, process it and save if (!empty($_POST)) { @@ -79,20 +91,16 @@ if (file_exists($filename)) { } } - - // if ladder or carry1 is 0, set empty field. - if (!$post['ladder']){ - $post['ladder'] = ''; - } - if (!$post['carry1']){ - $post['carry1'] = ''; - } - - - - + if ($_POST['formtype'] == "uniqueitems") { - $saver->save($u); + // if ladder or carry1 is 0, set empty field. + if (!$post['ladder']) { + $post['ladder'] = ''; + } + if (!$post['carry1']) { + $post['carry1'] = ''; + } + $saver->save($u, $post); $saver->saveTblEnries("UniqueItems.tbl.txt"); } if ($_POST['formtype'] == "setitems") { @@ -100,13 +108,13 @@ if (file_exists($filename)) { - $saver->save($s); + $saver->save($s, $post); $saver->saveTblEnries("SetItems.tbl.txt"); } } -// load app +// load app require_once './src/index.php'; } else { header('Location: ./src/D2Config.php'); diff --git a/src/D2Database.php b/src/D2Database.php new file mode 100644 index 0000000..17994bc --- /dev/null +++ b/src/D2Database.php @@ -0,0 +1,74 @@ + + + This file is part of D2IM. + + D2IM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + D2IM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with D2IM. If not, see . + */ + +class D2Database { + + public function __construct() { + PDO_Connect("sqlite:" . DB_FILE); + } + + public function createTables($file, $data) { + $tableName = basename($file); + $tableName = strtolower(substr($tableName, 0, -4)); + $sql = "CREATE TABLE IF NOT EXISTS `$tableName` ("; + foreach ($data[0] as $k => $v) { + if (is_numeric($v)) { + $dataType = "INT"; + } else { + $dataType = "VARCHAR(255)"; + } + $sql .= "`$k` $dataType NOT NULL,"; + } + $sql = rtrim($sql, ","); + $sql .= ")"; + $res = PDO_Execute($sql); + } + + public function fillsTables($file, $data) { + $tableName = basename($file); + $tableName = strtolower(substr($tableName, 0, -4)); + + foreach ($data as $d) { + $sql = "INSERT INTO `$tableName` ("; + if (!empty($d)) { + foreach ($d as $k => $v) { + $sql .= "`$k`" . ","; + } + } + $sql = rtrim($sql, ","); + $sql .= ") "; + + $sql .= "VALUES ("; + + if (!empty($d)) { + foreach ($d as $k => $v) { + $sql .= '"' . $v . '"' . ","; + } + } + $sql = rtrim($sql, ","); + $sql .= ");"; + + var_dump($sql); + PDO_Execute($sql); + } + } + +} diff --git a/src/D2Functions.php b/src/D2Functions.php index 81dc656..6312739 100644 --- a/src/D2Functions.php +++ b/src/D2Functions.php @@ -1,6 +1,6 @@ "; var_dump($var); echo ""; diff --git a/src/D2Gems.php b/src/D2Gems.php new file mode 100644 index 0000000..79b1eb9 --- /dev/null +++ b/src/D2Gems.php @@ -0,0 +1,41 @@ + + + This file is part of D2IM. + + D2IM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + D2IM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with D2IM. If not, see . + */ +?> +

Gem Maker

+
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/D2SaveFile.php b/src/D2SaveFile.php index d920732..0edcd71 100644 --- a/src/D2SaveFile.php +++ b/src/D2SaveFile.php @@ -24,13 +24,10 @@ class D2SaveFile { public $path; public function save($file, $data) { - + $fp = fopen($this->path.DIRECTORY_SEPARATOR.$file, 'a+'); - - $this->saveBackup($file); - - fputcsv($fp, $data, "\t"); - + $this->saveBackup($file); + fputcsv($fp, $data, "\t"); } diff --git a/src/D2TxtParser.php b/src/D2TxtParser.php index 528de97..e7926a0 100644 --- a/src/D2TxtParser.php +++ b/src/D2TxtParser.php @@ -26,10 +26,10 @@ class D2TxtParser { public $path = TXT_PATH; - // Files specific to Unique Items - + public $db; + public function __construct() { - + $this->db = new D2Database(); } public function parseFile($file) { @@ -46,10 +46,10 @@ class D2TxtParser { $allProps[] = $d['code']; } $filteredProps = array_diff($allProps, $propsToFilter); - return $filteredProps; + return $filteredProps; } - public function parseData($file) { + public function parseData($file) { $file = $this->path . $file; $rows = array_map(function ($v) { return str_getcsv($v, "\t"); @@ -58,9 +58,12 @@ class D2TxtParser { $header = array_shift($rows); foreach ($rows as $row) { $data[] = @array_combine($header, $row); - } + } + $this->db->createTables($file, $data); + $this->db->fillsTables($file, $data); return $data; } + } ?> \ No newline at end of file diff --git a/src/D2UM.php b/src/D2UM.php index 0118d65..3094995 100644 --- a/src/D2UM.php +++ b/src/D2UM.php @@ -203,8 +203,8 @@
diff --git a/src/header.php b/src/header.php index 04f5805..341961d 100644 --- a/src/header.php +++ b/src/header.php @@ -38,6 +38,9 @@ +
@@ -47,4 +50,7 @@
+
+ +
\ No newline at end of file diff --git a/txt/SetItems.txt b/txt/SetItems.txt index 8ab1cfa..4424f2b 100644 --- a/txt/SetItems.txt +++ b/txt/SetItems.txt @@ -421,3 +421,10 @@ Set24.4 Set24 9ts Harpoon 7 30 51 5 5000 2 dmg% 190 240 stack 75 75 rep Set25.1 Set25 rin Ring 1 45 45 5 5000 2 hp 35 50 mana 25 25 mag% 200 200 regen -20 -20 regen -15 -15 extra-fire 100 100 0 Set25.2 Set25 rin Ring 1 45 45 5 5000 2 hp 35 50 mana 25 25 mag% 200 200 regen -20 -20 regen -15 -15 extra-ltng 100 100 0 Set25.3 Set25 amu Amulet 1 45 45 5 5000 2 hp 50 75 mana 40 40 mag% 200 200 regen -20 -20 regen -15 -15 extra-cold 100 100 0 +"Set 1item" "set 1" ghm "Great Helm" 1 2 3 inv flippy 12 213 2 0 on setitems +"Set 1item" "set 1" ghm "Great Helm" 1 2 3 inv flippy 12 213 2 0 on setitems +"Set 1item" "set 1" ghm "Great Helm" 1 2 3 inv flippy 12 213 2 0 on setitems +"Set 1item" "set 1" ghm "Great Helm" 1 2 3 inv flippy 12 213 2 0 on setitems +"Set 1item" "set 1" ghm "Great Helm" 1 2 3 inv flippy 12 213 2 0 on setitems +"Set 1item" "set 1" ghm "Great Helm" 1 2 3 inv flippy 12 213 2 0 on setitems +"Set 1item" "set 1" ghm "Great Helm" 1 2 3 inv flippy 12 213 2 0 on setitems diff --git a/vendor/rb-sqlite.php b/vendor/rb-sqlite.php new file mode 100644 index 0000000..0f97666 --- /dev/null +++ b/vendor/rb-sqlite.php @@ -0,0 +1,17019 @@ +mode === self::C_LOGGER_ECHO ) { + echo $log; + } else { + $this->logs[] = $log; + } + } else { + if ( $this->mode === self::C_LOGGER_ECHO ) { + echo $argument; + } else { + $this->logs[] = $argument; + } + } + + if ( $this->mode === self::C_LOGGER_ECHO ) echo "
" . PHP_EOL; + } + } + + /** + * Returns the internal log array. + * The internal log array is where all log messages are stored. + * + * @return array + */ + public function getLogs() + { + return $this->logs; + } + + /** + * Clears the internal log array, removing all + * previously stored entries. + * + * @return self + */ + public function clear() + { + $this->logs = array(); + return $this; + } + + /** + * Selects a logging mode. + * There are several options available. + * + * * C_LOGGER_ARRAY - log silently, stores entries in internal log array only + * * C_LOGGER_ECHO - also forward log messages directly to STDOUT + * + * @param integer $mode mode of operation for logging object + * + * @return self + */ + public function setMode( $mode ) + { + if ($mode !== self::C_LOGGER_ARRAY && $mode !== self::C_LOGGER_ECHO ) { + throw new RedException( 'Invalid mode selected for logger, use C_LOGGER_ARRAY or C_LOGGER_ECHO.' ); + } + $this->mode = $mode; + return $this; + } + + /** + * Searches for all log entries in internal log array + * for $needle and returns those entries. + * This method will return an array containing all matches for your + * search query. + * + * @param string $needle phrase to look for in internal log array + * + * @return array + */ + public function grep( $needle ) + { + $found = array(); + foreach( $this->logs as $logEntry ) { + if ( strpos( $logEntry, $needle ) !== FALSE ) $found[] = $logEntry; + } + return $found; + } +} +} + +namespace RedBeanPHP\Logger\RDefault { + +use RedBeanPHP\Logger as Logger; +use RedBeanPHP\Logger\RDefault as RDefault; +use RedBeanPHP\RedException as RedException; + +/** + * Debug logger. + * A special logger for debugging purposes. + * Provides debugging logging functions for RedBeanPHP. + * + * @file RedBeanPHP/Logger/RDefault/Debug.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Debug extends RDefault implements Logger +{ + /** + * @var integer + */ + protected $strLen = 40; + + /** + * @var boolean + */ + protected static $noCLI = FALSE; + + /** + * @var boolean + */ + protected $flagUseStringOnlyBinding = FALSE; + + /** + * Toggles CLI override. By default debugging functions will + * output differently based on PHP_SAPI values. This function + * allows you to override the PHP_SAPI setting. If you set + * this to TRUE, CLI output will be supressed in favour of + * HTML output. So, to get HTML on the command line use + * setOverrideCLIOutput( TRUE ). + * + * @param boolean $yesNo CLI-override setting flag + * + * @return void + */ + public static function setOverrideCLIOutput( $yesNo ) + { + self::$noCLI = $yesNo; + } + + /** + * Writes a query for logging with all bindings / params filled + * in. + * + * @param string $newSql the query + * @param array $newBindings the bindings to process (key-value pairs) + * + * @return string + */ + protected function writeQuery( $newSql, $newBindings ) + { + //avoid str_replace collisions: slot1 and slot10 (issue 407). + uksort( $newBindings, function( $a, $b ) { + return ( strlen( $b ) - strlen( $a ) ); + } ); + + $newStr = $newSql; + foreach( $newBindings as $slot => $value ) { + if ( strpos( $slot, ':' ) === 0 ) { + $newStr = str_replace( $slot, $this->fillInValue( $value ), $newStr ); + } + } + return $newStr; + } + + /** + * Fills in a value of a binding and truncates the + * resulting string if necessary. + * + * @param mixed $value bound value + * + * @return string + */ + protected function fillInValue( $value ) + { + if ( is_array( $value ) && count( $value ) == 2 ) { + $paramType = end( $value ); + $value = reset( $value ); + } else { + $paramType = NULL; + } + + if ( is_null( $value ) ) $value = 'NULL'; + + if ( $this->flagUseStringOnlyBinding ) $paramType = \PDO::PARAM_STR; + + if ( $paramType != \PDO::PARAM_INT && $paramType != \PDO::PARAM_STR ) { + if ( \RedBeanPHP\QueryWriter\AQueryWriter::canBeTreatedAsInt( $value ) || $value === 'NULL') { + $paramType = \PDO::PARAM_INT; + } else { + $paramType = \PDO::PARAM_STR; + } + } + + if ( strlen( $value ) > ( $this->strLen ) ) { + $value = substr( $value, 0, ( $this->strLen ) ).'... '; + } + + if ($paramType === \PDO::PARAM_STR) { + $value = '\''.$value.'\''; + } + + return $value; + } + + /** + * Dependending on the current mode of operation, + * this method will either log and output to STDIN or + * just log. + * + * Depending on the value of constant PHP_SAPI this function + * will format output for console or HTML. + * + * @param string $str string to log or output and log + * + * @return void + */ + protected function output( $str ) + { + $this->logs[] = $str; + if ( !$this->mode ) { + $highlight = FALSE; + /* just a quick heuritsic to highlight schema changes */ + if ( strpos( $str, 'CREATE' ) === 0 + || strpos( $str, 'ALTER' ) === 0 + || strpos( $str, 'DROP' ) === 0) { + $highlight = TRUE; + } + if (PHP_SAPI === 'cli' && !self::$noCLI) { + if ($highlight) echo "\e[91m"; + echo $str, PHP_EOL; + echo "\e[39m"; + } else { + if ($highlight) { + echo "{$str}"; + } else { + echo $str; + } + echo '
'; + } + } + } + + /** + * Normalizes the slots in an SQL string. + * Replaces question mark slots with :slot1 :slot2 etc. + * + * @param string $sql sql to normalize + * + * @return string + */ + protected function normalizeSlots( $sql ) + { + $newSql = $sql; + $i = 0; + while(strpos($newSql, '?') !== FALSE ){ + $pos = strpos( $newSql, '?' ); + $slot = ':slot'.$i; + $begin = substr( $newSql, 0, $pos ); + $end = substr( $newSql, $pos+1 ); + if (PHP_SAPI === 'cli' && !self::$noCLI) { + $newSql = "{$begin}\e[32m{$slot}\e[39m{$end}"; + } else { + $newSql = "{$begin}$slot{$end}"; + } + $i ++; + } + return $newSql; + } + + /** + * Normalizes the bindings. + * Replaces numeric binding keys with :slot1 :slot2 etc. + * + * @param array $bindings bindings to normalize + * + * @return array + */ + protected function normalizeBindings( $bindings ) + { + $i = 0; + $newBindings = array(); + foreach( $bindings as $key => $value ) { + if ( is_numeric($key) ) { + $newKey = ':slot'.$i; + $newBindings[$newKey] = $value; + $i++; + } else { + $newBindings[$key] = $value; + } + } + return $newBindings; + } + + /** + * Logger method. + * + * Takes a number of arguments tries to create + * a proper debug log based on the available data. + * + * @return void + */ + public function log() + { + if ( func_num_args() < 1 ) return; + + $sql = func_get_arg( 0 ); + + if ( func_num_args() < 2) { + $bindings = array(); + } else { + $bindings = func_get_arg( 1 ); + } + + if ( !is_array( $bindings ) ) { + return $this->output( $sql ); + } + + $newSql = $this->normalizeSlots( $sql ); + $newBindings = $this->normalizeBindings( $bindings ); + $newStr = $this->writeQuery( $newSql, $newBindings ); + $this->output( $newStr ); + } + + /** + * Sets the max string length for the parameter output in + * SQL queries. Set this value to a reasonable number to + * keep you SQL queries readable. + * + * @param integer $len string length + * + * @return self + */ + public function setParamStringLength( $len = 20 ) + { + $this->strLen = max(0, $len); + return $this; + } + + /** + * Whether to bind all parameters as strings. + * If set to TRUE this will cause all integers to be bound as STRINGS. + * This will NOT affect NULL values. + * + * @param boolean $yesNo pass TRUE to bind all parameters as strings. + * + * @return self + */ + public function setUseStringOnlyBinding( $yesNo = false ) + { + $this->flagUseStringOnlyBinding = (boolean) $yesNo; + return $this; + } +} +} + +namespace RedBeanPHP { + +/** + * Interface for database drivers. + * The Driver API conforms to the ADODB pseudo standard + * for database drivers. + * + * @file RedBeanPHP/Driver.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Driver +{ + /** + * Runs a query and fetches results as a multi dimensional array. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array + */ + public function GetAll( $sql, $bindings = array() ); + + /** + * Runs a query and fetches results as a column. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array + */ + public function GetCol( $sql, $bindings = array() ); + + /** + * Runs a query and returns results as a single cell. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return mixed + */ + public function GetOne( $sql, $bindings = array() ); + + /** + * Runs a query and returns results as an associative array + * indexed by the first column. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return mixed + */ + public function GetAssocRow( $sql, $bindings = array() ); + + /** + * Runs a query and returns a flat array containing the values of + * one row. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array + */ + public function GetRow( $sql, $bindings = array() ); + + /** + * Executes SQL code and allows key-value binding. + * This function allows you to provide an array with values to bind + * to query parameters. For instance you can bind values to question + * marks in the query. Each value in the array corresponds to the + * question mark in the query that matches the position of the value in the + * array. You can also bind values using explicit keys, for instance + * array(":key"=>123) will bind the integer 123 to the key :key in the + * SQL. This method has no return value. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array Affected Rows + */ + public function Execute( $sql, $bindings = array() ); + + /** + * Returns the latest insert ID if driver does support this + * feature. + * + * @return integer + */ + public function GetInsertID(); + + /** + * Returns the number of rows affected by the most recent query + * if the currently selected driver driver supports this feature. + * + * @return integer + */ + public function Affected_Rows(); + + /** + * Returns a cursor-like object from the database. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return mixed + */ + public function GetCursor( $sql, $bindings = array() ); + + /** + * Toggles debug mode. In debug mode the driver will print all + * SQL to the screen together with some information about the + * results. + * + * This method is for more fine-grained control. Normally + * you should use the facade to start the query debugger for + * you. The facade will manage the object wirings necessary + * to use the debugging functionality. + * + * Usage (through facade): + * + * + * R::debug( TRUE ); + * ...rest of program... + * R::debug( FALSE ); + * + * + * The example above illustrates how to use the RedBeanPHP + * query debugger through the facade. + * + * @param boolean $trueFalse turn on/off + * @param Logger $logger logger instance + * + * @return void + */ + public function setDebugMode( $tf, $customLogger ); + + /** + * Starts a transaction. + * + * @return void + */ + public function CommitTrans(); + + /** + * Commits a transaction. + * + * @return void + */ + public function StartTrans(); + + /** + * Rolls back a transaction. + * + * @return void + */ + public function FailTrans(); + + /** + * Resets the internal Query Counter. + * + * @return self + */ + public function resetCounter(); + + /** + * Returns the number of SQL queries processed. + * + * @return integer + */ + public function getQueryCount(); + + /** + * Sets initialization code for connection. + * + * @param callable $code code + * + * @return void + */ + public function setInitCode( $code ); + + /** + * Returns the version string from the database server. + * + * @return string + */ + public function DatabaseServerVersion(); +} +} + +namespace RedBeanPHP\Driver { + +use RedBeanPHP\Driver as Driver; +use RedBeanPHP\Logger as Logger; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\RedException\SQL as SQL; +use RedBeanPHP\Logger\RDefault as RDefault; +use RedBeanPHP\PDOCompatible as PDOCompatible; +use RedBeanPHP\Cursor\PDOCursor as PDOCursor; + +/** + * PDO Driver + * This Driver implements the RedBean Driver API. + * for RedBeanPHP. This is the standard / default database driver + * for RedBeanPHP. + * + * @file RedBeanPHP/PDO.php + * @author Gabor de Mooij and the RedBeanPHP Community, Desfrenes + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) Desfrenes & Gabor de Mooij and the RedBeanPHP community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class RPDO implements Driver +{ + /** + * @var integer + */ + protected $max; + + /** + * @var string + */ + protected $dsn; + + /** + * @var boolean + */ + protected $loggingEnabled = FALSE; + + /** + * @var Logger + */ + protected $logger = NULL; + + /** + * @var PDO + */ + protected $pdo; + + /** + * @var integer + */ + protected $affectedRows; + + /** + * @var integer + */ + protected $resultArray; + + /** + * @var array + */ + protected $connectInfo = array(); + + /** + * @var boolean + */ + protected $isConnected = FALSE; + + /** + * @var bool + */ + protected $flagUseStringOnlyBinding = FALSE; + + /** + * @var integer + */ + protected $queryCounter = 0; + + /** + * @var string + */ + protected $mysqlCharset = ''; + + /** + * @var string + */ + protected $mysqlCollate = ''; + + /** + * @var boolean + */ + protected $stringifyFetches = TRUE; + + /** + * @var string + */ + protected $initSQL = NULL; + + /** + * @var callable + */ + protected $initCode = NULL; + + /** + * Binds parameters. This method binds parameters to a PDOStatement for + * Query Execution. This method binds parameters as NULL, INTEGER or STRING + * and supports both named keys and question mark keys. + * + * @param PDOStatement $statement PDO Statement instance + * @param array $bindings values that need to get bound to the statement + * + * @return void + */ + protected function bindParams( $statement, $bindings ) + { + foreach ( $bindings as $key => &$value ) { + $k = is_integer( $key ) ? $key + 1 : $key; + + if ( is_array( $value ) && count( $value ) == 2 ) { + $paramType = end( $value ); + $value = reset( $value ); + } else { + $paramType = NULL; + } + + if ( is_null( $value ) ) { + $statement->bindValue( $k, NULL, \PDO::PARAM_NULL ); + continue; + } + + if ( $paramType != \PDO::PARAM_INT && $paramType != \PDO::PARAM_STR ) { + if ( !$this->flagUseStringOnlyBinding && AQueryWriter::canBeTreatedAsInt( $value ) && abs( $value ) <= $this->max ) { + $paramType = \PDO::PARAM_INT; + } else { + $paramType = \PDO::PARAM_STR; + } + } + + $statement->bindParam( $k, $value, $paramType ); + } + } + + /** + * This method runs the actual SQL query and binds a list of parameters to the query. + * slots. The result of the query will be stored in the protected property + * $rs (always array). The number of rows affected (result of rowcount, if supported by database) + * is stored in protected property $affectedRows. If the debug flag is set + * this function will send debugging output to screen buffer. + * + * @param string $sql the SQL string to be send to database server + * @param array $bindings the values that need to get bound to the query slots + * @param array $options + * + * @return mixed + * @throws SQL + */ + protected function runQuery( $sql, $bindings, $options = array() ) + { + $this->connect(); + if ( $this->loggingEnabled && $this->logger ) { + $this->logger->log( $sql, $bindings ); + } + try { + if ( strpos( 'pgsql', $this->dsn ) === 0 ) { + if (defined('\\PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT')) { + $statement = @$this->pdo->prepare($sql, array(\PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT => TRUE)); + } else { + $statement = $this->pdo->prepare($sql); + } + } else { + $statement = $this->pdo->prepare( $sql ); + } + $this->bindParams( $statement, $bindings ); + $statement->execute(); + $this->queryCounter ++; + $this->affectedRows = $statement->rowCount(); + if ( $statement->columnCount() ) { + $fetchStyle = ( isset( $options['fetchStyle'] ) ) ? $options['fetchStyle'] : NULL; + if ( isset( $options['noFetch'] ) && $options['noFetch'] ) { + $this->resultArray = array(); + return $statement; + } + $this->resultArray = $statement->fetchAll( $fetchStyle ); + if ( $this->loggingEnabled && $this->logger ) { + $this->logger->log( 'resultset: ' . count( $this->resultArray ) . ' rows' ); + } + } else { + $this->resultArray = array(); + } + } catch ( \PDOException $e ) { + //Unfortunately the code field is supposed to be int by default (php) + //So we need a property to convey the SQL State code. + $err = $e->getMessage(); + if ( $this->loggingEnabled && $this->logger ) $this->logger->log( 'An error occurred: ' . $err ); + $exception = new SQL( $err, 0, $e ); + $exception->setSQLState( $e->getCode() ); + $exception->setDriverDetails( $e->errorInfo ); + throw $exception; + } + } + + /** + * Try to fix MySQL character encoding problems. + * MySQL < 5.5.3 does not support proper 4 byte unicode but they + * seem to have added it with version 5.5.3 under a different label: utf8mb4. + * We try to select the best possible charset based on your version data. + * + * @return void + */ + protected function setEncoding() + { + $driver = $this->pdo->getAttribute( \PDO::ATTR_DRIVER_NAME ); + if ($driver === 'mysql') { + $charset = $this->hasCap( 'utf8mb4' ) ? 'utf8mb4' : 'utf8'; + $collate = $this->hasCap( 'utf8mb4_520' ) ? '_unicode_520_ci' : '_unicode_ci'; + $this->pdo->setAttribute(\PDO::MYSQL_ATTR_INIT_COMMAND, 'SET NAMES '. $charset ); //on every re-connect + /* #624 removed space before SET NAMES because it causes trouble with ProxySQL */ + $this->pdo->exec('SET NAMES '. $charset); //also for current connection + $this->mysqlCharset = $charset; + $this->mysqlCollate = $charset . $collate; + } + } + + /** + * Determine if a database supports a particular feature. + * Currently this function can be used to detect the following features: + * + * - utf8mb4 + * - utf8mb4 520 + * + * Usage: + * + * + * $this->hasCap( 'utf8mb4_520' ); + * + * + * By default, RedBeanPHP uses this method under the hood to make sure + * you use the latest UTF8 encoding possible for your database. + * + * @param $db_cap identifier of database capability + * + * @return int|false Whether the database feature is supported, FALSE otherwise. + **/ + protected function hasCap( $db_cap ) + { + $compare = FALSE; + $version = $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ); + switch ( strtolower( $db_cap ) ) { + case 'utf8mb4': + //oneliner, to boost code coverage (coverage does not span versions) + 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 ( strpos( $client_version, 'mysqlnd' ) !== FALSE ) { + $client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version ); + $compare = version_compare( $client_version, '5.0.9', '>=' ); + } else { + $compare = version_compare( $client_version, '5.5.3', '>=' ); + } + break; + case 'utf8mb4_520': + $compare = version_compare( $version, '5.6', '>=' ); + break; + } + + return $compare; + } + + /** + * Constructor. You may either specify dsn, user and password or + * just give an existing PDO connection. + * + * Usage: + * + * + * $driver = new RPDO( $dsn, $user, $password ); + * + * + * The example above illustrates how to create a driver + * instance from a database connection string (dsn), a username + * and a password. It's also possible to pass a PDO object. + * + * Usage: + * + * + * $driver = new RPDO( $existingConnection ); + * + * + * The second example shows how to create an RPDO instance + * from an existing PDO object. + * + * @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, $options = array() ) + { + if ( is_object( $dsn ) ) { + $this->pdo = $dsn; + $this->isConnected = TRUE; + $this->setEncoding(); + $this->pdo->setAttribute( \PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION ); + $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE,\PDO::FETCH_ASSOC ); + // make sure that the dsn at least contains the type + $this->dsn = $this->getDatabaseType(); + } else { + $this->dsn = $dsn; + $this->connectInfo = array( 'pass' => $pass, 'user' => $user ); + if (is_array($options)) $this->connectInfo['options'] = $options; + } + + //PHP 5.3 PDO SQLite has a bug with large numbers: + if ( ( strpos( $this->dsn, 'sqlite' ) === 0 && PHP_MAJOR_VERSION === 5 && PHP_MINOR_VERSION === 3 ) || defined('HHVM_VERSION') || $this->dsn === 'test-sqlite-53' ) { + $this->max = 2147483647; //otherwise you get -2147483648 ?! demonstrated in build #603 on Travis. + } elseif ( strpos( $this->dsn, 'cubrid' ) === 0 ) { + $this->max = 2147483647; //bindParam in pdo_cubrid also fails... + } else { + $this->max = PHP_INT_MAX; //the normal value of course (makes it possible to use large numbers in LIMIT clause) + } + } + + /** + * Sets PDO in stringify fetch mode. + * If set to TRUE, this method will make sure all data retrieved from + * the database will be fetched as a string. Default: TRUE. + * + * To set it to FALSE... + * + * Usage: + * + * + * R::getDatabaseAdapter()->getDatabase()->stringifyFetches( FALSE ); + * + * + * Important! + * Note, this method only works if you set the value BEFORE the connection + * has been establish. Also, this setting ONLY works with SOME drivers. + * It's up to the driver to honour this setting. + * + * @param boolean $bool + */ + public function stringifyFetches( $bool ) { + $this->stringifyFetches = $bool; + } + + /** + * Returns the best possible encoding for MySQL based on version data. + * This method can be used to obtain the best character set parameters + * possible for your database when constructing a table creation query + * containing clauses like: CHARSET=... COLLATE=... + * This is a MySQL-specific method and not part of the driver interface. + * + * Usage: + * + * + * $charset_collate = $this->adapter->getDatabase()->getMysqlEncoding( TRUE ); + * + * + * @param boolean $retCol pass TRUE to return both charset/collate + * + * @return string|array + */ + public function getMysqlEncoding( $retCol = FALSE ) + { + if( $retCol ) + return array( 'charset' => $this->mysqlCharset, 'collate' => $this->mysqlCollate ); + return $this->mysqlCharset; + } + + /** + * Whether to bind all parameters as strings. + * If set to TRUE this will cause all integers to be bound as STRINGS. + * This will NOT affect NULL values. + * + * @param boolean $yesNo pass TRUE to bind all parameters as strings. + * + * @return void + */ + public function setUseStringOnlyBinding( $yesNo ) + { + $this->flagUseStringOnlyBinding = (boolean) $yesNo; + if ( $this->loggingEnabled && $this->logger && method_exists($this->logger,'setUseStringOnlyBinding')) { + $this->logger->setUseStringOnlyBinding( $this->flagUseStringOnlyBinding ); + } + } + + /** + * 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; + } + + /** + * Sets initialization code to execute upon connecting. + * + * @param callable $code + * + * @return void + */ + public function setInitCode($code) + { + $this->initCode= $code; + } + + /** + * Establishes a connection to the database using PHP\PDO + * functionality. If a connection has already been established this + * method will simply return directly. This method also turns on + * UTF8 for the database and PDO-ERRMODE-EXCEPTION as well as + * PDO-FETCH-ASSOC. + * + * @return void + */ + public function connect() + { + if ( $this->isConnected ) return; + try { + $user = $this->connectInfo['user']; + $pass = $this->connectInfo['pass']; + $options = array(); + if (isset($this->connectInfo['options']) && is_array($this->connectInfo['options'])) { + $options = $this->connectInfo['options']; + } + $this->pdo = new \PDO( $this->dsn, $user, $pass, $options ); + $this->setEncoding(); + $this->pdo->setAttribute( \PDO::ATTR_STRINGIFY_FETCHES, $this->stringifyFetches ); + //cant pass these as argument to constructor, CUBRID driver does not understand... + $this->pdo->setAttribute( \PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION ); + $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC ); + $this->isConnected = TRUE; + /* run initialisation query if any */ + if ( $this->initSQL !== NULL ) { + $this->Execute( $this->initSQL ); + $this->initSQL = NULL; + } + if ( $this->initCode !== NULL ) { + $code = $this->initCode; + $code( $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ) ); + } + } catch ( \PDOException $exception ) { + $matches = array(); + $dbname = ( preg_match( '/dbname=(\w+)/', $this->dsn, $matches ) ) ? $matches[1] : '?'; + throw new \PDOException( 'Could not connect to database (' . $dbname . ').', $exception->getCode() ); + } + } + + /** + * Directly sets PDO instance into driver. + * This method might improve performance, however since the driver does + * not configure this instance terrible things may happen... only use + * this method if you are an expert on RedBeanPHP, PDO and UTF8 connections and + * you know your database server VERY WELL. + * + * - connected TRUE|FALSE (treat this instance as connected, default: TRUE) + * - setEncoding TRUE|FALSE (let RedBeanPHP set encoding for you, default: TRUE) + * - setAttributes TRUE|FALSE (let RedBeanPHP set attributes for you, default: TRUE)* + * - setDSNString TRUE|FALSE (extract DSN string from PDO instance, default: TRUE) + * - stringFetch TRUE|FALSE (whether you want to stringify fetches or not, default: TRUE) + * - runInitCode TRUE|FALSE (run init code if any, default: TRUE) + * + * *attributes: + * - RedBeanPHP will ask database driver to throw Exceptions on errors (recommended for compatibility) + * - RedBeanPHP will ask database driver to use associative arrays when fetching (recommended for compatibility) + * + * @param PDO $pdo PDO instance + * @param array $options Options to apply + * + * @return void + */ + public function setPDO( \PDO $pdo, $options = array() ) { + $this->pdo = $pdo; + + $connected = TRUE; + $setEncoding = TRUE; + $setAttributes = TRUE; + $setDSNString = TRUE; + $runInitCode = TRUE; + $stringFetch = TRUE; + + if ( isset($options['connected']) ) $connected = $options['connected']; + if ( isset($options['setEncoding']) ) $setEncoding = $options['setEncoding']; + if ( isset($options['setAttributes']) ) $setAttributes = $options['setAttributes']; + if ( isset($options['setDSNString']) ) $setDSNString = $options['setDSNString']; + if ( isset($options['runInitCode']) ) $runInitCode = $options['runInitCode']; + if ( isset($options['stringFetch']) ) $stringFetch = $options['stringFetch']; + + if ($connected) $this->connected = $connected; + if ($setEncoding) $this->setEncoding(); + if ($setAttributes) { + $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_STRINGIFY_FETCHES, $stringFetch ); + } + if ($runInitCode) { + /* run initialisation query if any */ + if ( $this->initSQL !== NULL ) { + $this->Execute( $this->initSQL ); + $this->initSQL = NULL; + } + if ( $this->initCode !== NULL ) { + $code = $this->initCode; + $code( $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ) ); + } + } + if ($setDSNString) $this->dsn = $this->getDatabaseType(); + } + + /** + * @see Driver::GetAll + */ + public function GetAll( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings ); + return $this->resultArray; + } + + /** + * @see Driver::GetAssocRow + */ + public function GetAssocRow( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings, array( + 'fetchStyle' => \PDO::FETCH_ASSOC + ) + ); + return $this->resultArray; + } + + /** + * @see Driver::GetCol + */ + public function GetCol( $sql, $bindings = array() ) + { + $rows = $this->GetAll( $sql, $bindings ); + + if ( empty( $rows ) || !is_array( $rows ) ) { + return array(); + } + + $cols = array(); + foreach ( $rows as $row ) { + $cols[] = reset( $row ); + } + + return $cols; + } + + /** + * @see Driver::GetOne + */ + public function GetOne( $sql, $bindings = array() ) + { + $arr = $this->GetAll( $sql, $bindings ); + + if ( empty( $arr[0] ) || !is_array( $arr[0] ) ) { + return NULL; + } + + return reset( $arr[0] ); + } + + /** + * Alias for getOne(). + * Backward compatibility. + * + * @param string $sql SQL + * @param array $bindings bindings + * + * @return mixed + */ + public function GetCell( $sql, $bindings = array() ) + { + return $this->GetOne( $sql, $bindings ); + } + + /** + * @see Driver::GetRow + */ + public function GetRow( $sql, $bindings = array() ) + { + $arr = $this->GetAll( $sql, $bindings ); + + if ( is_array( $arr ) && count( $arr ) ) { + return reset( $arr ); + } + + return array(); + } + + /** + * @see Driver::Excecute + */ + public function Execute( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings ); + return $this->affectedRows; + } + + /** + * @see Driver::GetInsertID + */ + public function GetInsertID() + { + $this->connect(); + + return (int) $this->pdo->lastInsertId(); + } + + /** + * @see Driver::GetCursor + */ + public function GetCursor( $sql, $bindings = array() ) + { + $statement = $this->runQuery( $sql, $bindings, array( 'noFetch' => TRUE ) ); + $cursor = new PDOCursor( $statement, \PDO::FETCH_ASSOC ); + return $cursor; + } + + /** + * @see Driver::Affected_Rows + */ + public function Affected_Rows() + { + $this->connect(); + return (int) $this->affectedRows; + } + + /** + * @see Driver::setDebugMode + */ + public function setDebugMode( $tf, $logger = NULL ) + { + $this->connect(); + $this->loggingEnabled = (bool) $tf; + if ( $this->loggingEnabled and !$logger ) { + $logger = new RDefault(); + } + $this->setLogger( $logger ); + } + + /** + * Injects Logger object. + * Sets the logger instance you wish to use. + * + * This method is for more fine-grained control. Normally + * you should use the facade to start the query debugger for + * you. The facade will manage the object wirings necessary + * to use the debugging functionality. + * + * Usage (through facade): + * + * + * R::debug( TRUE ); + * ...rest of program... + * R::debug( FALSE ); + * + * + * The example above illustrates how to use the RedBeanPHP + * query debugger through the facade. + * + * @param Logger $logger the logger instance to be used for logging + * + * @return self + */ + public function setLogger( Logger $logger ) + { + $this->logger = $logger; + return $this; + } + + /** + * Gets Logger object. + * Returns the currently active Logger instance. + * + * @return Logger + */ + public function getLogger() + { + return $this->logger; + } + + /** + * @see Driver::StartTrans + */ + public function StartTrans() + { + $this->connect(); + $this->pdo->beginTransaction(); + } + + /** + * @see Driver::CommitTrans + */ + public function CommitTrans() + { + $this->connect(); + $this->pdo->commit(); + } + + /** + * @see Driver::FailTrans + */ + public function FailTrans() + { + $this->connect(); + $this->pdo->rollback(); + } + + /** + * Returns the name of database driver for PDO. + * Uses the PDO attribute DRIVER NAME to obtain the name of the + * PDO driver. Use this method to identify the current PDO driver + * used to provide access to the database. Example of a database + * driver string: + * + * + * mysql + * + * + * Usage: + * + * + * echo R::getDatabaseAdapter()->getDatabase()->getDatabaseType(); + * + * + * The example above prints the current database driver string to + * stdout. + * + * Note that this is a driver-specific method, not part of the + * driver interface. This method might not be available in other + * drivers since it relies on PDO. + * + * @return string + */ + public function getDatabaseType() + { + $this->connect(); + return $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME ); + } + + /** + * Returns the version identifier string of the database client. + * This method can be used to identify the currently installed + * database client. Note that this method will also establish a connection + * (because this is required to obtain the version information). + * + * Example of a version string: + * + * + * mysqlnd 5.0.12-dev - 20150407 - $Id: b5c5906d452ec590732a93b051f3827e02749b83 $ + * + * + * Usage: + * + * + * echo R::getDatabaseAdapter()->getDatabase()->getDatabaseVersion(); + * + * + * The example above will print the version string to stdout. + * + * Note that this is a driver-specific method, not part of the + * driver interface. This method might not be available in other + * drivers since it relies on PDO. + * + * To obtain the database server version, use getDatabaseServerVersion() + * instead. + * + * @return mixed + */ + public function getDatabaseVersion() + { + $this->connect(); + return $this->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION ); + } + + /** + * Returns the underlying PHP PDO instance. + * For some low-level database operations you'll need access to the PDO + * object. Not that this method is only available in RPDO and other + * PDO based database drivers for RedBeanPHP. Other drivers may not have + * a method like this. The following example demonstrates how to obtain + * a reference to the PDO instance from the facade: + * + * Usage: + * + * + * $pdo = R::getDatabaseAdapter()->getDatabase()->getPDO(); + * + * + * @return PDO + */ + public function getPDO() + { + $this->connect(); + return $this->pdo; + } + + /** + * Closes the database connection. + * While database connections are closed automatically at the end of the PHP script, + * closing database connections is generally recommended to improve performance. + * Closing a database connection will immediately return the resources to PHP. + * + * Usage: + * + * + * R::setup( ... ); + * ... do stuff ... + * R::close(); + * + * + * @return void + */ + public function close() + { + $this->pdo = NULL; + $this->isConnected = FALSE; + } + + /** + * Returns TRUE if the current PDO instance is connected. + * + * @return boolean + */ + public function isConnected() + { + return $this->isConnected && $this->pdo; + } + + /** + * Toggles logging, enables or disables logging. + * + * @param boolean $enable TRUE to enable logging + * + * @return self + */ + public function setEnableLogging( $enable ) + { + $this->loggingEnabled = (boolean) $enable; + return $this; + } + + /** + * Resets the query counter. + * The query counter can be used to monitor the number + * of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * R::resetQueryCount(); + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return self + */ + public function resetCounter() + { + $this->queryCounter = 0; + return $this; + } + + /** + * Returns the number of SQL queries processed. + * This method returns the number of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * echo R::getQueryCount() . ' 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; + } + + /** + * Returns the version string from the database server. + * + * @return string + */ + public function DatabaseServerVersion() { + return trim( strval( $this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION) ) ); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\RedException as RedException; + +/** + * PHP 5.3 compatibility + * We extend JsonSerializable to avoid namespace conflicts, + * can't define interface with special namespace in PHP + */ +if (interface_exists('\JsonSerializable')) { interface Jsonable extends \JsonSerializable {}; } else { interface Jsonable {}; } + +/** + * OODBBean (Object Oriented DataBase Bean). + * + * to exchange information with the database. A bean represents + * a single table row and offers generic services for interaction + * with databases systems as well as some meta-data. + * + * @file RedBeanPHP/OODBBean.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * @desc OODBBean represents a bean. RedBeanPHP uses beans + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class OODBBean implements \IteratorAggregate,\ArrayAccess,\Countable,Jsonable +{ + /** + * FUSE error modes. + */ + const C_ERR_IGNORE = FALSE; + const C_ERR_LOG = 1; + const C_ERR_NOTICE = 2; + const C_ERR_WARN = 3; + const C_ERR_EXCEPTION = 4; + const C_ERR_FUNC = 5; + const C_ERR_FATAL = 6; + + /** + * @var boolean + */ + protected static $useFluidCount = FALSE; + + /** + * @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(); + + /** + * If this is set to TRUE, the __toString function will + * encode all properties as UTF-8 to repair invalid UTF-8 + * encodings and prevent exceptions (which are uncatchable from within + * a __toString-function). + * + * @var boolean + */ + protected static $enforceUTF8encoding = FALSE; + + /** + * This is where the real properties of the bean live. They are stored and retrieved + * by the magic getter and setter (__get and __set). + * + * @var array $properties + */ + protected $properties = array(); + + /** + * Here we keep the meta data of a bean. + * + * @var array + */ + protected $__info = array(); + + /** + * The BeanHelper allows the bean to access the toolbox objects to implement + * rich functionality, otherwise you would have to do everything with R or + * external objects. + * + * @var BeanHelper + */ + protected $beanHelper = NULL; + + /** + * @var null + */ + protected $fetchType = NULL; + + /** + * @var string + */ + protected $withSql = ''; + + /** + * @var array + */ + protected $withParams = array(); + + /** + * @var string + */ + protected $aliasName = NULL; + + /** + * @var string + */ + protected $via = NULL; + + /** + * @var boolean + */ + protected $noLoad = FALSE; + + /** + * @var boolean + */ + protected $all = FALSE; + + /** + * If fluid count is set to TRUE then $bean->ownCount() will + * return 0 if the table does not exists. + * Only for backward compatibility. + * Returns previouds value. + * + * @param boolean $toggle toggle + * + * @return boolean + */ + public static function useFluidCount( $toggle ) + { + $old = self::$useFluidCount; + self::$useFluidCount = $toggle; + return $old; + } + + /** + * 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; + } + + /** + * Return list of global aliases + * + * @return array + */ + public static function getAliases() + { + return self::$aliases; + } + + /** + * 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; + if ( $property == 'type' && !empty($bean->beanHelper)) { + $bean->__info['model'] = $bean->beanHelper->getModelForBean( $bean ); + } + } + return $beans; + } + + /** + * Accesses the shared list of a bean. + * To access beans that have been associated with the current bean + * using a many-to-many relationship use sharedXList where + * X is the type of beans in the list. + * + * Usage: + * + * + * $person = R::load( 'person', $id ); + * $friends = $person->sharedFriendList; + * + * + * The code snippet above demonstrates how to obtain all beans of + * type 'friend' that have associated using an N-M relation. + * This is a private method used by the magic getter / accessor. + * The example illustrates usage through these accessors. + * + * @param string $type the name of the list you want to retrieve + * @param OODB $redbean instance of the RedBeanPHP OODB class + * @param ToolBox $toolbox instance of ToolBox (to get access to core objects) + * + * @return array + */ + private function getSharedList( $type, $redbean, $toolbox ) + { + $writer = $toolbox->getWriter(); + if ( $this->via ) { + $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) ); + if ( $oldName !== $this->via ) { + //set the new renaming rule + $writer->renameAssocTable( $oldName, $this->via ); + } + $this->via = NULL; + } + $beans = array(); + if ($this->getID()) { + $type = $this->beau( $type ); + $assocManager = $redbean->getAssociationManager(); + $beans = $assocManager->related( $this, $type, $this->withSql, $this->withParams ); + } + return $beans; + } + + /** + * Accesses the ownList. The 'own' list contains beans + * associated using a one-to-many relation. The own-lists can + * be accessed through the magic getter/setter property + * ownXList where X is the type of beans in that list. + * + * Usage: + * + * + * $book = R::load( 'book', $id ); + * $pages = $book->ownPageList; + * + * + * The example above demonstrates how to access the + * pages associated with the book. Since this is a private method + * meant to be used by the magic accessors, the example uses the + * magic getter instead. + * + * @param string $type name of the list you want to retrieve + * @param OODB $oodb The RB OODB object database instance + * + * @return array + */ + private function getOwnList( $type, $redbean ) + { + $type = $this->beau( $type ); + if ( $this->aliasName ) { + $parentField = $this->aliasName; + $myFieldLink = $parentField . '_id'; + + $this->__info['sys.alias.' . $type] = $this->aliasName; + + $this->aliasName = NULL; + } else { + $parentField = $this->__info['type']; + $myFieldLink = $parentField . '_id'; + } + $beans = array(); + if ( $this->getID() ) { + reset( $this->withParams ); + $firstKey = count( $this->withParams ) > 0 + ? key( $this->withParams ) + : 0; + if ( is_int( $firstKey ) ) { + $sql = "{$myFieldLink} = ? {$this->withSql}"; + $bindings = array_merge( array( $this->getID() ), $this->withParams ); + } else { + $sql = "{$myFieldLink} = :slot0 {$this->withSql}"; + $bindings = $this->withParams; + $bindings[':slot0'] = $this->getID(); + } + $beans = $redbean->find( $type, array(), $sql, $bindings ); + } + foreach ( $beans as $beanFromList ) { + $beanFromList->__info['sys.parentcache.' . $parentField] = $this; + } + return $beans; + } + + /** + * Initializes a bean. Used by OODB for dispensing beans. + * It is not recommended to use this method to initialize beans. Instead + * use the OODB object to dispense new beans. You can use this method + * if you build your own bean dispensing mechanism. + * This is not recommended. + * + * Unless you know what you are doing, do NOT use this method. + * This is for advanced users only! + * + * @param string $type type of the new bean + * @param BeanHelper $beanhelper bean helper to obtain a toolbox and a model + * + * @return void + */ + public function initializeForDispense( $type, $beanhelper = NULL ) + { + $this->beanHelper = $beanhelper; + $this->__info['type'] = $type; + $this->__info['sys.id'] = 'id'; + $this->__info['sys.orig'] = array( 'id' => 0 ); + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + $this->__info['changelist'] = array(); + if ( $beanhelper ) { + $this->__info['model'] = $this->beanHelper->getModelForBean( $this ); + } + $this->properties['id'] = 0; + } + + /** + * Sets the Bean Helper. Normally the Bean Helper is set by OODB. + * Here you can change the Bean Helper. The Bean Helper is an object + * providing access to a toolbox for the bean necessary to retrieve + * nested beans (bean lists: ownBean, sharedBean) without the need to + * rely on static calls to the facade (or make this class dep. on OODB). + * + * @param BeanHelper $helper helper to use for this bean + * + * @return void + */ + public function setBeanHelper( BeanHelper $helper ) + { + $this->beanHelper = $helper; + } + + /** + * Returns an ArrayIterator so you can treat the bean like + * an array with the properties container as its contents. + * This method is meant for PHP and allows you to access beans as if + * they were arrays, i.e. using array notation: + * + * + * $bean[$key] = $value; + * + * + * Note that not all PHP functions work with the array interface. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator( $this->properties ); + } + + /** + * Imports all values from an associative array $array. Chainable. + * This method imports the values in the first argument as bean + * propery and value pairs. Use the second parameter to provide a + * selection. If a selection array is passed, only the entries + * having keys mentioned in the selection array will be imported. + * Set the third parameter to TRUE to preserve spaces in selection keys. + * + * @param array $array what you want to import + * @param string|array $selection selection of values + * @param boolean $notrim if TRUE selection keys will NOT be trimmed + * + * @return OODBBean + */ + public function import( $array, $selection = FALSE, $notrim = FALSE ) + { + if ( is_string( $selection ) ) { + $selection = explode( ',', $selection ); + } + if ( is_array( $selection ) ) { + if ( $notrim ) { + $selected = array_flip($selection); + } else { + $selected = array(); + foreach ( $selection as $key => $select ) { + $selected[trim( $select )] = TRUE; + } + } + } else { + $selected = FALSE; + } + foreach ( $array as $key => $value ) { + if ( $key != '__info' ) { + if ( !$selected || isset( $selected[$key] ) ) { + if ( is_array($value ) ) { + if ( isset( $value['_type'] ) ) { + $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $value['_type'] ); + unset( $value['_type'] ); + $bean->import($value); + $this->$key = $bean; + } else { + $listBeans = array(); + foreach( $value as $listKey => $listItem ) { + $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $listItem['_type'] ); + unset( $listItem['_type'] ); + $bean->import($listItem); + $list = &$this->$key; + $list[ $listKey ] = $bean; + } + } + } else { + $this->$key = $value; + } + } + } + } + return $this; + } + + /** + * Imports an associative array directly into the + * internal property array of the bean as well as the + * meta property sys.orig and sets the changed flag to FALSE. + * This is used by the repository objects to inject database rows + * into the beans. It is not recommended to use this method outside + * of a bean repository. + * + * @param array $row a database row + * + * @return self + */ + public function importRow( $row ) + { + $this->properties = $row; + $this->__info['sys.orig'] = $row; + $this->__info['changed'] = FALSE; + $this->__info['changelist'] = array(); + return $this; + } + + /** + * Imports data from another bean. Chainable. + * Copies the properties from the source bean to the internal + * property list. + * + * Usage: + * + * + * $copy->importFrom( $bean ); + * + * + * The example above demonstrates how to make a shallow copy + * of a bean using the importFrom() method. + * + * @param OODBBean $sourceBean the source bean to take properties from + * + * @return OODBBean + */ + public function importFrom( OODBBean $sourceBean ) + { + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + $this->properties = $sourceBean->properties; + + return $this; + } + + /** + * Injects the properties of another bean but keeps the original ID. + * Just like import() but keeps the original ID. + * Chainable. + * + * @param OODBBean $otherBean the bean whose properties you would like to copy + * + * @return OODBBean + */ + public function inject( OODBBean $otherBean ) + { + $myID = $this->properties['id']; + $this->import( $otherBean->export( FALSE, FALSE, TRUE ) ); + $this->id = $myID; + + return $this; + } + + /** + * Exports the bean as an array. + * This function exports the contents of a bean to an array and returns + * the resulting array. Depending on the parameters you can also + * export an entire graph of beans, apply filters or exclude meta data. + * + * Usage: + * + * + * $bookData = $book->export( TRUE, TRUE, FALSE, [ 'author' ] ); + * + * + * The example above exports all bean properties to an array + * called $bookData including its meta data, parent objects but without + * any beans of type 'author'. + * + * @param boolean $meta set to TRUE if you want to export meta data as well + * @param boolean $parents set to TRUE if you want to export parents as well + * @param boolean $onlyMe set to TRUE if you want to export only this bean + * @param array $filters optional whitelist for export + * + * @return array + */ + public function export( $meta = FALSE, $parents = FALSE, $onlyMe = FALSE, $filters = array() ) + { + $arr = array(); + if ( $parents ) { + foreach ( $this as $key => $value ) { + if ( substr( $key, -3 ) != '_id' ) continue; + + $prop = substr( $key, 0, strlen( $key ) - 3 ); + $this->$prop; + } + } + $hasFilters = is_array( $filters ) && count( $filters ); + foreach ( $this as $key => $value ) { + if ( !$onlyMe && is_array( $value ) ) { + $vn = array(); + + foreach ( $value as $i => $b ) { + if ( !( $b instanceof OODBBean ) ) continue; + $vn[] = $b->export( $meta, FALSE, FALSE, $filters ); + $value = $vn; + } + } elseif ( $value instanceof OODBBean ) { if ( $hasFilters ) { //has to be on one line, otherwise code coverage miscounts as miss + if ( !in_array( strtolower( $value->getMeta( 'type' ) ), $filters ) ) continue; + } + $value = $value->export( $meta, $parents, FALSE, $filters ); + } + $arr[$key] = $value; + } + if ( $meta ) { + $arr['__info'] = $this->__info; + } + return $arr; + } + + /** + * Implements isset() function for use as an array. + * This allows you to use isset() on bean properties. + * + * Usage: + * + * + * $book->title = 'my book'; + * echo isset($book['title']); //TRUE + * + * + * The example illustrates how one can apply the + * isset() function to a bean. + * + * @param string $property name of the property you want to check + * + * @return boolean + */ + public function __isset( $property ) + { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + } + return isset( $this->properties[$property] ); + } + + /** + * Checks whether a related bean exists. + * For instance if a post bean has a related author, this method + * can be used to check if the author is set without loading the author. + * This method works by checking the related ID-field. + * + * @param string $property name of the property you wish to check + * + * @return boolean + */ + public function exists( $property ) + { + $property = $this->beau( $property ); + /* fixes issue #549, see Base/Bean test */ + $hiddenRelationField = "{$property}_id"; + if ( array_key_exists( $hiddenRelationField, $this->properties ) ) { + if ( !is_null( $this->properties[$hiddenRelationField] ) ) { + return TRUE; + } + } + return FALSE; + } + + /** + * Returns the ID of the bean. + * If for some reason the ID has not been set, this method will + * return NULL. This is actually the same as accessing the + * id property using $bean->id. The ID of a bean is it's primary + * key and should always correspond with a table column named + * 'id'. + * + * @return string|null + */ + public function getID() + { + return ( isset( $this->properties['id'] ) ) ? (string) $this->properties['id'] : NULL; + } + + /** + * Unsets a property of a bean. + * Magic method, gets called implicitly when + * performing the unset() operation + * on a bean property. + * + * @param string $property property to unset + * + * @return void + */ + public function __unset( $property ) + { + $property = $this->beau( $property ); + + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + } + unset( $this->properties[$property] ); + $shadowKey = 'sys.shadow.'.$property; + if ( isset( $this->__info[ $shadowKey ] ) ) unset( $this->__info[$shadowKey] ); + //also clear modifiers + $this->clearModifiers(); + return; + } + + /** + * Adds WHERE clause conditions to ownList retrieval. + * For instance to get the pages that belong to a book you would + * issue the following command: $book->ownPage + * However, to order these pages by number use: + * + * + * $book->with(' ORDER BY `number` ASC ')->ownPage + * + * + * the additional SQL snippet will be merged into the final + * query. + * + * @param string $sql SQL to be added to retrieval query. + * @param array $bindings array with parameters to bind to SQL snippet + * + * @return OODBBean + */ + public function with( $sql, $bindings = array() ) + { + $this->withSql = $sql; + $this->withParams = $bindings; + return $this; + } + + /** + * Just like with(). Except that this method prepends the SQL query snippet + * with AND which makes it slightly more comfortable to use a conditional + * SQL snippet. For instance to filter an own-list with pages (belonging to + * a book) on specific chapters you can use: + * + * $book->withCondition(' chapter = 3 ')->ownPage + * + * This will return in the own list only the pages having 'chapter == 3'. + * + * @param string $sql SQL to be added to retrieval query (prefixed by AND) + * @param array $bindings array with parameters to bind to SQL snippet + * + * @return OODBBean + */ + public function withCondition( $sql, $bindings = array() ) + { + $this->withSql = ' AND ' . $sql; + $this->withParams = $bindings; + return $this; + } + + /** + * Tells the bean to (re)load the following list without any + * conditions. If you have an ownList or sharedList with a + * condition you can use this method to reload the entire list. + * + * Usage: + * + * + * $bean->with( ' LIMIT 3 ' )->ownPage; //Just 3 + * $bean->all()->ownPage; //Reload all pages + * + * + * @return self + */ + public function all() + { + $this->all = TRUE; + return $this; + } + + /** + * Tells the bean to only access the list but not load + * its contents. Use this if you only want to add something to a list + * and you have no interest in retrieving its contents from the database. + * + * Usage: + * + * + * $book->noLoad()->ownPage[] = $newPage; + * + * + * In the example above we add the $newPage bean to the + * page list of book without loading all the pages first. + * If you know in advance that you are not going to use + * the contents of the list, you may use the noLoad() modifier + * to make sure the queries required to load the list will not + * be executed. + * + * @return self + */ + public function noLoad() + { + $this->noLoad = TRUE; + return $this; + } + + /** + * Prepares an own-list to use an alias. This is best explained using + * an example. Imagine a project and a person. The project always involves + * two persons: a teacher and a student. The person beans have been aliased in this + * case, so to the project has a teacher_id pointing to a person, and a student_id + * also pointing to a person. Given a project, we obtain the teacher like this: + * + * + * $project->fetchAs('person')->teacher; + * + * + * Now, if we want all projects of a teacher we cant say: + * + * + * $teacher->ownProject + * + * + * because the $teacher is a bean of type 'person' and no project has been + * assigned to a person. Instead we use the alias() method like this: + * + * + * $teacher->alias('teacher')->ownProject + * + * + * now we get the projects associated with the person bean aliased as + * a teacher. + * + * @param string $aliasName the alias name to use + * + * @return OODBBean + */ + public function alias( $aliasName ) + { + $this->aliasName = $this->beau( $aliasName ); + return $this; + } + + /** + * Returns properties of bean as an array. + * This method returns the raw internal property list of the + * bean. Only use this method for optimization purposes. Otherwise + * use the export() method to export bean data to arrays. + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Returns properties of bean as an array. + * This method returns the raw internal property list of the + * bean. Only use this method for optimization purposes. Otherwise + * use the export() method to export bean data to arrays. + * This method returns an array with the properties array and + * the type (string). + * + * @return array + */ + public function getPropertiesAndType() + { + return array( $this->properties, $this->__info['type'] ); + } + + /** + * Turns a camelcase property name into an underscored property name. + * + * Examples: + * + * - oneACLRoute -> one_acl_route + * - camelCase -> camel_case + * + * Also caches the result to improve performance. + * + * @param string $property property to un-beautify + * + * @return string + */ + public function beau( $property ) + { + static $beautifulColumns = array(); + + if ( ctype_lower( $property ) ) return $property; + if ( + ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) + || ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) + || ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) + ) { + + $property = preg_replace( '/List$/', '', $property ); + return $property; + } + if ( !isset( $beautifulColumns[$property] ) ) { + $beautifulColumns[$property] = AQueryWriter::camelsSnake( $property ); + } + return $beautifulColumns[$property]; + } + + /** + * Modifiers are a powerful concept in RedBeanPHP, they make it possible + * to change the way a property has to be loaded. + * RedBeanPHP uses property modifiers using a prefix notation like this: + * + * + * $book->fetchAs('page')->cover; + * + * + * Here, we load a bean of type page, identified by the cover property + * (or cover_id in the database). Because the modifier is called before + * the property is accessed, the modifier must be remembered somehow, + * this changes the state of the bean. Accessing a property causes the + * bean to clear its modifiers. To clear the modifiers manually you can + * use this method. + * + * Usage: + * + * + * $book->with( 'LIMIT 1' ); + * $book->clearModifiers()->ownPageList; + * + * + * In the example above, the 'LIMIT 1' clause is + * cleared before accessing the pages of the book, causing all pages + * to be loaded in the list instead of just one. + * + * @return self + */ + public function clearModifiers() + { + $this->withSql = ''; + $this->withParams = array(); + $this->aliasName = NULL; + $this->fetchType = NULL; + $this->noLoad = FALSE; + $this->all = FALSE; + $this->via = NULL; + return $this; + } + + /** + * Determines whether a list is opened in exclusive mode or not. + * If a list has been opened in exclusive mode this method will return TRUE, + * othwerwise it will return FALSE. + * + * @param string $listName name of the list to check + * + * @return boolean + */ + public function isListInExclusiveMode( $listName ) + { + $listName = $this->beau( $listName ); + + if ( strpos( $listName, 'xown' ) === 0 && ctype_upper( substr( $listName, 4, 1 ) ) ) { + $listName = substr($listName, 1); + } + $listName = lcfirst( substr( $listName, 3 ) ); + return ( isset( $this->__info['sys.exclusive-'.$listName] ) && $this->__info['sys.exclusive-'.$listName] ); + } + + /** + * Magic Getter. Gets the value for a specific property in the bean. + * If the property does not exist this getter will make sure no error + * occurs. This is because RedBean allows you to query (probe) for + * properties. If the property can not be found this method will + * return NULL instead. + * + * Usage: + * + * + * $title = $book->title; + * $pages = $book->ownPageList; + * $tags = $book->sharedTagList; + * + * + * The example aboves lists several ways to invoke the magic getter. + * You can use the magic setter to access properties, own-lists, + * exclusive own-lists (xownLists) and shared-lists. + * + * @param string $property name of the property you wish to obtain the value of + * + * @return mixed + */ + public function &__get( $property ) + { + $isEx = FALSE; + $isOwn = FALSE; + $isShared = FALSE; + if ( !ctype_lower( $property ) ) { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + $listName = lcfirst( substr( $property, 3 ) ); + $isEx = TRUE; + $isOwn = TRUE; + $this->__info['sys.exclusive-'.$listName] = TRUE; + } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) { + $isOwn = TRUE; + $listName = lcfirst( substr( $property, 3 ) ); + } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) { + $isShared = TRUE; + } + } + $fieldLink = $property . '_id'; + $exists = isset( $this->properties[$property] ); + + //If not exists and no field link and no list, bail out. + if ( !$exists && !isset($this->$fieldLink) && (!$isOwn && !$isShared )) { + $this->clearModifiers(); + /** + * Github issue: + * Remove $NULL to directly return NULL #625 + * @@ -1097,8 +1097,7 @@ public function &__get( $property ) + * $this->all = FALSE; + * $this->via = NULL; + * + * - $NULL = NULL; + * - return $NULL; + * + return NULL; + * + * leads to regression: + * PHP Stack trace: + * PHP 1. {main}() testje.php:0 + * PHP 2. RedBeanPHP\OODBBean->__get() testje.php:22 + * Notice: Only variable references should be returned by reference in rb.php on line 2529 + */ + $NULL = NULL; + return $NULL; + } + + $hasAlias = (!is_null($this->aliasName)); + $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ? + ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE; + $hasSQL = ($this->withSql !== '' || $this->via !== NULL); + $hasAll = (boolean) ($this->all); + + //If exists and no list or exits and list not changed, bail out. + if ( $exists && ((!$isOwn && !$isShared ) || (!$hasSQL && !$differentAlias && !$hasAll)) ) { + $this->clearModifiers(); + return $this->properties[$property]; + } + + list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox(); + + //If it's another bean, then we load it and return + if ( isset( $this->$fieldLink ) ) { + $this->__info['tainted'] = TRUE; + if ( isset( $this->__info["sys.parentcache.$property"] ) ) { + $bean = $this->__info["sys.parentcache.$property"]; + } else { + if ( isset( self::$aliases[$property] ) ) { + $type = self::$aliases[$property]; + } elseif ( $this->fetchType ) { + $type = $this->fetchType; + $this->fetchType = NULL; + } else { + $type = $property; + } + $bean = NULL; + if ( !is_null( $this->properties[$fieldLink] ) ) { + $bean = $redbean->load( $type, $this->properties[$fieldLink] ); + } + } + $this->properties[$property] = $bean; + $this->clearModifiers(); + return $this->properties[$property]; + } + + /* Implicit: elseif ( $isOwn || $isShared ) */ + if ( $this->noLoad ) { + $beans = array(); + } elseif ( $isOwn ) { + $beans = $this->getOwnList( $listName, $redbean ); + } else { + $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox ); + } + $this->properties[$property] = $beans; + $this->__info["sys.shadow.$property"] = $beans; + $this->__info['tainted'] = TRUE; + + $this->clearModifiers(); + return $this->properties[$property]; + + } + + /** + * Magic Setter. Sets the value for a specific property. + * This setter acts as a hook for OODB to mark beans as tainted. + * The tainted meta property can be retrieved using getMeta("tainted"). + * The tainted meta property indicates whether a bean has been modified and + * can be used in various caching mechanisms. + * + * @param string $property name of the property you wish to assign a value to + * @param mixed $value the value you want to assign + * + * @return void + */ + public function __set( $property, $value ) + { + $isEx = FALSE; + $isOwn = FALSE; + $isShared = FALSE; + + if ( !ctype_lower( $property ) ) { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + $listName = lcfirst( substr( $property, 3 ) ); + $isEx = TRUE; + $isOwn = TRUE; + $this->__info['sys.exclusive-'.$listName] = TRUE; + } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) { + $isOwn = TRUE; + $listName = lcfirst( substr( $property, 3 ) ); + } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) { + $isShared = TRUE; + } + } elseif ( self::$convertArraysToJSON && is_array( $value ) ) { + $value = json_encode( $value ); + } + + $hasAlias = (!is_null($this->aliasName)); + $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ? + ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE; + $hasSQL = ($this->withSql !== '' || $this->via !== NULL); + $exists = isset( $this->properties[$property] ); + $fieldLink = $property . '_id'; + $isFieldLink = (($pos = strrpos($property, '_id')) !== FALSE) && array_key_exists( ($fieldName = substr($property, 0, $pos)), $this->properties ); + + + if ( ($isOwn || $isShared) && (!$exists || $hasSQL || $differentAlias) ) { + + if ( !$this->noLoad ) { + list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox(); + if ( $isOwn ) { + $beans = $this->getOwnList( $listName, $redbean ); + } else { + $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox ); + } + $this->__info["sys.shadow.$property"] = $beans; + } + } + + $this->clearModifiers(); + + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + array_push( $this->__info['changelist'], $property ); + + if ( array_key_exists( $fieldLink, $this->properties ) && !( $value instanceof OODBBean ) ) { + if ( is_null( $value ) || $value === FALSE ) { + + unset( $this->properties[ $property ]); + $this->properties[ $fieldLink ] = NULL; + + return; + } else { + throw new RedException( 'Cannot cast to bean.' ); + } + } + + if ( $isFieldLink ){ + unset( $this->properties[ $fieldName ]); + $this->properties[ $property ] = NULL; + } + + + if ( $value === FALSE ) { + $value = '0'; + } elseif ( $value === TRUE ) { + $value = '1'; + /* for some reason there is some kind of bug in xdebug so that it doesnt count this line otherwise... */ + } elseif ( $value instanceof \DateTime ) { $value = $value->format( 'Y-m-d H:i:s' ); } + $this->properties[$property] = $value; + } + + /** + * @deprecated + * + * Sets a property of the bean allowing you to keep track of + * the state yourself. This method sets a property of the bean and + * allows you to control how the state of the bean will be affected. + * + * While there may be some circumstances where this method is needed, + * this method is considered to be extremely dangerous. + * This method is only for advanced users. + * + * @param string $property property + * @param mixed $value value + * @param boolean $updateShadow whether you want to update the shadow + * @param boolean $taint whether you want to mark the bean as tainted + * + * @return void + */ + public function setProperty( $property, $value, $updateShadow = FALSE, $taint = FALSE ) + { + $this->properties[$property] = $value; + + if ( $updateShadow ) { + $this->__info['sys.shadow.' . $property] = $value; + } + + if ( $taint ) { + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + } + } + + /** + * Returns the value of a meta property. A meta property + * contains additional information about the bean object that will not + * be stored in the database. Meta information is used to instruct + * RedBeanPHP as well as other systems how to deal with the bean. + * If the property cannot be found this getter will return NULL instead. + * + * Example: + * + * + * $bean->setMeta( 'flush-cache', TRUE ); + * + * + * RedBeanPHP also stores meta data in beans, this meta data uses + * keys prefixed with 'sys.' (system). + * + * @param string $path path to property in meta data + * @param mixed $default default value + * + * @return mixed + */ + public function getMeta( $path, $default = NULL ) + { + return ( isset( $this->__info[$path] ) ) ? $this->__info[$path] : $default; + } + + /** + * Returns a value from the data bundle. + * The data bundle might contain additional data send from an SQL query, + * for instance, the total number of rows. If the property cannot be + * found, the default value will be returned. If no default has + * been specified, this method returns NULL. + * + * @param string $key key + * @param mixed $default default (defaults to NULL) + * + * @return mixed; + */ + public function info( $key, $default = NULL ) { + return ( isset( $this->__info['data.bundle'][$key] ) ) ? $this->__info['data.bundle'][$key] : $default; + } + + /** + * Gets and unsets a meta property. + * Moves a meta property out of the bean. + * This is a short-cut method that can be used instead + * of combining a get/unset. + * + * @param string $path path to property in meta data + * @param mixed $default default value + * + * @return mixed + */ + public function moveMeta( $path, $value = NULL ) + { + if ( isset( $this->__info[$path] ) ) { + $value = $this->__info[ $path ]; + unset( $this->__info[ $path ] ); + } + return $value; + } + + /** + * Stores a value in the specified Meta information property. + * The first argument should be the key to store the value under, + * the second argument should be the value. It is common to use + * a path-like notation for meta data in RedBeanPHP like: + * 'my.meta.data', however the dots are purely for readability, the + * meta data methods do not store nested structures or hierarchies. + * + * @param string $path path / key to store value under + * @param mixed $value value to store in bean (not in database) as meta data + * + * @return OODBBean + */ + public function setMeta( $path, $value ) + { + $this->__info[$path] = $value; + if ( $path == 'type' && !empty($this->beanHelper)) { + $this->__info['model'] = $this->beanHelper->getModelForBean( $this ); + } + + return $this; + } + + /** + * Copies the meta information of the specified bean + * This is a convenience method to enable you to + * exchange meta information easily. + * + * @param OODBBean $bean bean to copy meta data of + * + * @return OODBBean + */ + public function copyMetaFrom( OODBBean $bean ) + { + $this->__info = $bean->__info; + + return $this; + } + + /** + * Sends the call to the registered model. + * This method can also be used to override bean behaviour. + * In that case you don't want an error or exception to be triggered + * if the method does not exist in the model (because it's optional). + * Unfortunately we cannot add an extra argument to __call() for this + * because the signature is fixed. Another option would be to set + * a special flag ( i.e. $this->isOptionalCall ) but that would + * cause additional complexity because we have to deal with extra temporary state. + * So, instead I allowed the method name to be prefixed with '@', in practice + * nobody creates methods like that - however the '@' symbol in PHP is widely known + * to suppress error handling, so we can reuse the semantics of this symbol. + * If a method name gets passed starting with '@' the overrideDontFail variable + * will be set to TRUE and the '@' will be stripped from the function name before + * attempting to invoke the method on the model. This way, we have all the + * logic in one place. + * + * @param string $method name of the method + * @param array $args argument list + * + * @return mixed + */ + public function __call( $method, $args ) + { + if ( empty( $this->__info['model'] ) ) { + return NULL; + } + + $overrideDontFail = FALSE; + if ( strpos( $method, '@' ) === 0 ) { + $method = substr( $method, 1 ); + $overrideDontFail = TRUE; + } + + if ( !is_callable( array( $this->__info['model'], $method ) ) ) { + + if ( self::$errorHandlingFUSE === FALSE || $overrideDontFail ) { + return NULL; + } + + if ( in_array( $method, array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ), TRUE ) ) { + return NULL; + } + + $message = "FUSE: method does not exist in model: $method"; + if ( self::$errorHandlingFUSE === self::C_ERR_LOG ) { + error_log( $message ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_NOTICE ) { + trigger_error( $message, E_USER_NOTICE ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_WARN ) { + trigger_error( $message, E_USER_WARNING ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_EXCEPTION ) { + throw new \Exception( $message ); + } elseif ( self::$errorHandlingFUSE === self::C_ERR_FUNC ) { + $func = self::$errorHandler; + return $func(array( + 'message' => $message, + 'method' => $method, + 'args' => $args, + 'bean' => $this + )); + } + trigger_error( $message, E_USER_ERROR ); + return NULL; + } + + return call_user_func_array( array( $this->__info['model'], $method ), $args ); + } + + /** + * Implementation of __toString Method + * Routes call to Model. If the model implements a __toString() method this + * method will be called and the result will be returned. In case of an + * echo-statement this result will be printed. If the model does not + * implement a __toString method, this method will return a JSON + * representation of the current bean. + * + * @return string + */ + public function __toString() + { + $string = $this->__call( '@__toString', array() ); + + if ( $string === NULL ) { + $list = array(); + foreach($this->properties as $property => $value) { + if (is_scalar($value)) { + if ( self::$enforceUTF8encoding ) { + $list[$property] = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); + } else { + $list[$property] = $value; + } + } + } + $data = json_encode( $list ); + return $data; + } else { + return $string; + } + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Call gets routed to __set. + * + * @param mixed $offset offset string + * @param mixed $value value + * + * @return void + */ + public function offsetSet( $offset, $value ) + { + $this->__set( $offset, $value ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return boolean + */ + public function offsetExists( $offset ) + { + return $this->__isset( $offset ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Unsets a value from the array/bean. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return void + */ + public function offsetUnset( $offset ) + { + $this->__unset( $offset ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Returns value of a property. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return mixed + */ + public function &offsetGet( $offset ) + { + return $this->__get( $offset ); + } + + /** + * Chainable method to cast a certain ID to a bean; for instance: + * $person = $club->fetchAs('person')->member; + * This will load a bean of type person using member_id as ID. + * + * @param string $type preferred fetch type + * + * @return OODBBean + */ + public function fetchAs( $type ) + { + $this->fetchType = $type; + + return $this; + } + + /** + * Prepares to load a bean using the bean type specified by + * another property. + * Similar to fetchAs but uses a column instead of a direct value. + * + * Usage: + * + * + * $car = R::load( 'car', $id ); + * $engine = $car->poly('partType')->part; + * + * + * In the example above, we have a bean of type car that + * may consists of several parts (i.e. chassis, wheels). + * To obtain the 'engine' we access the property 'part' + * using the type (i.e. engine) specified by the property + * indicated by the argument of poly(). + * This essentially is a polymorph relation, hence the name. + * In database this relation might look like this: + * + * partType | part_id + * -------------------- + * engine | 1020300 + * wheel | 4820088 + * chassis | 7823122 + * + * @param string $field field name to use for mapping + * + * @return OODBBean + */ + public function poly( $field ) + { + return $this->fetchAs( $this->$field ); + } + + /** + * Traverses a bean property with the specified function. + * Recursively iterates through the property invoking the + * function for each bean along the way passing the bean to it. + * + * Can be used together with with, withCondition, alias and fetchAs. + * + * + * $task + * ->withCondition(' priority >= ? ', [ $priority ]) + * ->traverse('ownTaskList', function( $t ) use ( &$todo ) { + * $todo[] = $t->descr; + * } ); + * + * + * In the example, we create a to-do list by traversing a + * hierarchical list of tasks while filtering out all tasks + * having a low priority. + * + * @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, $depth = 1 ) + { + $this->via = NULL; + if ( strpos( $property, 'shared' ) !== FALSE ) { + throw new RedException( 'Traverse only works with (x)own-lists.' ); + } + + if ( !is_null( $maxDepth ) ) { + if ( !$maxDepth-- ) return $this; + } + + $oldFetchType = $this->fetchType; + $oldAliasName = $this->aliasName; + $oldWith = $this->withSql; + $oldBindings = $this->withParams; + + $beans = $this->$property; + + if ( $beans === NULL ) return $this; + + if ( !is_array( $beans ) ) $beans = array( $beans ); + + foreach( $beans as $bean ) { + $function( $bean, $depth ); + $bean->fetchType = $oldFetchType; + $bean->aliasName = $oldAliasName; + $bean->withSql = $oldWith; + $bean->withParams = $oldBindings; + + $bean->traverse( $property, $function, $maxDepth, $depth + 1 ); + } + + return $this; + } + + /** + * Implementation of Countable interface. Makes it possible to use + * count() function on a bean. This method gets invoked if you use + * the count() function on a bean. The count() method will return + * the number of properties of the bean, this includes the id property. + * + * Usage: + * + * + * $bean = R::dispense('bean'); + * $bean->property1 = 1; + * $bean->property2 = 2; + * echo count($bean); //prints 3 (cause id is also a property) + * + * + * The example above will print the number 3 to stdout. + * Although we have assigned values to just two properties, the + * primary key id is also a property of the bean and together + * that makes 3. Besides using the count() function, you can also + * call this method using a method notation: $bean->count(). + * + * @return integer + */ + public function count() + { + return count( $this->properties ); + } + + /** + * Checks whether a bean is empty or not. + * A bean is empty if it has no other properties than the id field OR + * if all the other properties are 'empty()' (this might + * include NULL and FALSE values). + * + * Usage: + * + * + * $newBean = R::dispense( 'bean' ); + * $newBean->isEmpty(); // TRUE + * + * + * The example above demonstrates that newly dispensed beans are + * considered 'empty'. + * + * @return boolean + */ + public function isEmpty() + { + $empty = TRUE; + foreach ( $this->properties as $key => $value ) { + if ( $key == 'id' ) { + continue; + } + if ( !empty( $value ) ) { + $empty = FALSE; + } + } + + return $empty; + } + + /** + * Chainable setter. + * This method is actually the same as just setting a value + * using a magic setter (->property = ...). The difference + * is that you can chain these setters like this: + * + * Usage: + * + * + * $book->setAttr('title', 'mybook')->setAttr('author', 'me'); + * + * + * This is the same as setting both properties $book->title and + * $book->author. Sometimes a chained notation can improve the + * readability of the code. + * + * @param string $property the property of the bean + * @param mixed $value the value you want to set + * + * @return OODBBean + */ + public function setAttr( $property, $value ) + { + $this->$property = $value; + + return $this; + } + + /** + * Convience method. + * Unsets all properties in the internal properties array. + * + * Usage: + * + * + * $bean->property = 1; + * $bean->unsetAll( array( 'property' ) ); + * $bean->property; //NULL + * + * + * In the example above the 'property' of the bean will be + * unset, resulting in the getter returning NULL instead of 1. + * + * @param array $properties properties you want to unset. + * + * @return OODBBean + */ + public function unsetAll( $properties ) + { + foreach ( $properties as $prop ) { + if ( isset( $this->properties[$prop] ) ) { + unset( $this->properties[$prop] ); + } + } + return $this; + } + + /** + * Returns original (old) value of a property. + * You can use this method to see what has changed in a + * bean. The original value of a property is the value that + * this property has had since the bean has been retrieved + * from the databases. + * + * + * $book->title = 'new title'; + * $oldTitle = $book->old('title'); + * + * + * The example shows how to use the old() method. + * Here we set the title property of the bean to 'new title', then + * we obtain the original value using old('title') and store it in + * a variable $oldTitle. + * + * @param string $property name of the property you want the old value of + * + * @return mixed + */ + public function old( $property ) + { + $old = $this->getMeta( 'sys.orig', array() ); + + if ( array_key_exists( $property, $old ) ) { + return $old[$property]; + } + + return NULL; + } + + /** + * Convenience method. + * + * Returns TRUE if the bean has been changed, or FALSE otherwise. + * Same as $bean->getMeta('tainted'); + * Note that a bean becomes tainted as soon as you retrieve a list from + * the bean. This is because the bean lists are arrays and the bean cannot + * determine whether you have made modifications to a list so RedBeanPHP + * will mark the whole bean as tainted. + * + * @return boolean + */ + public function isTainted() + { + return $this->getMeta( 'tainted' ); + } + + /** + * Returns TRUE if the value of a certain property of the bean has been changed and + * FALSE otherwise. + * + * Note that this method will return TRUE if applied to a loaded list. + * Also note that this method keeps track of the bean's history regardless whether + * it has been stored or not. Storing a bean does not undo it's history, + * to clean the history of a bean use: clearHistory(). + * + * @param string $property name of the property you want the change-status of + * + * @return boolean + */ + public function hasChanged( $property ) + { + return ( array_key_exists( $property, $this->properties ) ) ? + $this->old( $property ) != $this->properties[$property] : FALSE; + } + + /** + * Returns TRUE if the specified list exists, has been loaded + * and has been changed: + * beans have been added or deleted. + * This method will not tell you anything about + * the state of the beans in the list. + * + * Usage: + * + * + * $book->hasListChanged( 'ownPage' ); // FALSE + * array_pop( $book->ownPageList ); + * $book->hasListChanged( 'ownPage' ); // TRUE + * + * + * In the example, the first time we ask whether the + * own-page list has been changed we get FALSE. Then we pop + * a page from the list and the hasListChanged() method returns TRUE. + * + * @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. + * + * Usage: + * + * + * $book->title = 'book'; + * echo $book->hasChanged( 'title' ); //TRUE + * R::store( $book ); + * echo $book->hasChanged( 'title' ); //TRUE + * $book->clearHistory(); + * echo $book->hasChanged( 'title' ); //FALSE + * + * + * Note that even after store(), the history of the bean still + * contains the act of changing the title of the book. + * Only after invoking clearHistory() will the history of the bean + * be cleared and will hasChanged() return FALSE. + * + * @return self + */ + public function clearHistory() + { + $this->__info['sys.orig'] = array(); + foreach( $this->properties as $key => $value ) { + if ( is_scalar($value) ) { + $this->__info['sys.orig'][$key] = $value; + } else { + $this->__info['sys.shadow.'.$key] = $value; + } + } + $this->__info[ 'changelist' ] = array(); + return $this; + } + + /** + * Creates a N-M relation by linking an intermediate bean. + * This method can be used to quickly connect beans using indirect + * relations. For instance, given an album and a song you can connect the two + * using a track with a number like this: + * + * Usage: + * + * + * $album->link('track', array('number'=>1))->song = $song; + * + * + * or: + * + * + * $album->link($trackBean)->song = $song; + * + * + * What this method does is adding the link bean to the own-list, in this case + * ownTrack. If the first argument is a string and the second is an array or + * a JSON string then the linking bean gets dispensed on-the-fly as seen in + * example #1. After preparing the linking bean, the bean is returned thus + * allowing the chained setter: ->song = $song. + * + * @param string|OODBBean $typeOrBean type of bean to dispense or the full bean + * @param string|array $qualification JSON string or array (optional) + * + * @return OODBBean + */ + public function link( $typeOrBean, $qualification = array() ) + { + if ( is_string( $typeOrBean ) ) { + $typeOrBean = AQueryWriter::camelsSnake( $typeOrBean ); + $bean = $this->beanHelper->getToolBox()->getRedBean()->dispense( $typeOrBean ); + if ( is_string( $qualification ) ) { + $data = json_decode( $qualification, TRUE ); + } else { + $data = $qualification; + } + foreach ( $data as $key => $value ) { + $bean->$key = $value; + } + } else { + $bean = $typeOrBean; + } + $list = 'own' . ucfirst( $bean->getMeta( 'type' ) ); + array_push( $this->$list, $bean ); + return $bean; + } + + /** + * Returns a bean of the given type with the same ID of as + * the current one. This only happens in a one-to-one relation. + * This is as far as support for 1-1 goes in RedBeanPHP. This + * method will only return a reference to the bean, changing it + * and storing the bean will not update the related one-bean. + * + * Usage: + * + * + * $author = R::load( 'author', $id ); + * $biography = $author->one( 'bio' ); + * + * + * The example loads the biography associated with the author + * using a one-to-one relation. These relations are generally not + * created (nor supported) by RedBeanPHP. + * + * @param $type type of bean to load + * + * @return OODBBean + */ + public function one( $type ) { + return $this->beanHelper + ->getToolBox() + ->getRedBean() + ->load( $type, $this->id ); + } + + /** + * Reloads the bean. + * Returns the same bean freshly loaded from the database. + * This method is equal to the following code: + * + * + * $id = $bean->id; + * $type = $bean->getMeta( 'type' ); + * $bean = R::load( $type, $id ); + * + * + * This is just a convenience method to reload beans + * quickly. + * + * Usage: + * + * + * R::exec( ...update query... ); + * $book = $book->fresh(); + * + * + * The code snippet above illustrates how to obtain changes + * caused by an UPDATE query, simply by reloading the bean using + * the fresh() method. + * + * @return OODBBean + */ + public function fresh() + { + return $this->beanHelper + ->getToolbox() + ->getRedBean() + ->load( $this->getMeta( 'type' ), $this->properties['id'] ); + } + + /** + * Registers a association renaming globally. + * Use via() and link() to associate shared beans using a + * 3rd bean that will act as an intermediate type. For instance + * consider an employee and a project. We could associate employees + * with projects using a sharedEmployeeList. But, maybe there is more + * to the relationship than just the association. Maybe we want + * to qualify the relation between a project and an employee with + * a role: 'developer', 'designer', 'tester' and so on. In that case, + * it might be better to introduce a new concept to reflect this: + * the participant. However, we still want the flexibility to + * query our employees in one go. This is where link() and via() + * can help. You can still introduce the more applicable + * concept (participant) and have your easy access to the shared beans. + * + * + * $Anna = R::dispense( 'employee' ); + * $Anna->badge = 'Anna'; + * $project = R::dispense( 'project' ); + * $project->name = 'x'; + * $Anna->link( 'participant', array( + * 'arole' => 'developer' + * ) )->project = $project; + * R::storeAll( array( $project, $Anna ) ); + * $employees = $project + * ->with(' ORDER BY badge ASC ') + * ->via( 'participant' ) + * ->sharedEmployee; + * + * + * This piece of code creates a project and an employee. + * It then associates the two using a via-relation called + * 'participant' ( employee <-> participant <-> project ). + * So, there will be a table named 'participant' instead of + * a table named 'employee_project'. Using the via() method, the + * employees associated with the project are retrieved 'via' + * the participant table (and an SQL snippet to order them by badge). + * + * @param string $via type you wish to use for shared lists + * + * @return OODBBean + */ + public function via( $via ) + { + $this->via = AQueryWriter::camelsSnake( $via ); + + return $this; + } + + /** + * Counts all own beans of type $type. + * Also works with alias(), with() and withCondition(). + * Own-beans or xOwn-beans (exclusively owned beans) are beans + * that have been associated using a one-to-many relation. They can + * be accessed through the ownXList where X is the type of the + * associated beans. + * + * Usage: + * + * + * $Bill->alias( 'author' ) + * ->countOwn( 'book' ); + * + * + * The example above counts all the books associated with 'author' + * $Bill. + * + * @param string $type the type of bean you want to count + * + * @return integer + */ + public function countOwn( $type ) + { + $type = $this->beau( $type ); + if ( $this->aliasName ) { + $myFieldLink = $this->aliasName . '_id'; + $this->aliasName = NULL; + } else { + $myFieldLink = $this->__info['type'] . '_id'; + } + $count = 0; + if ( $this->getID() ) { + reset( $this->withParams ); + $firstKey = count( $this->withParams ) > 0 + ? key( $this->withParams ) + : 0; + if ( is_int( $firstKey ) ) { + $sql = "{$myFieldLink} = ? {$this->withSql}"; + $bindings = array_merge( array( $this->getID() ), $this->withParams ); + } else { + $sql = "{$myFieldLink} = :slot0 {$this->withSql}"; + $bindings = $this->withParams; + $bindings[':slot0'] = $this->getID(); + } + if ( !self::$useFluidCount ) { + $count = $this->beanHelper->getToolbox()->getWriter()->queryRecordCount( $type, array(), $sql, $bindings ); + } else { + $count = $this->beanHelper->getToolbox()->getRedBean()->count( $type, $sql, $bindings ); + } + } + $this->clearModifiers(); + return (int) $count; + } + + /** + * Counts all shared beans of type $type. + * Also works with via(), with() and withCondition(). + * Shared beans are beans that have an many-to-many relation. + * They can be accessed using the sharedXList, where X the + * type of the shared bean. + * + * Usage: + * + * + * $book = R::dispense( 'book' ); + * $book->sharedPageList = R::dispense( 'page', 5 ); + * R::store( $book ); + * echo $book->countShared( 'page' ); + * + * + * The code snippet above will output '5', because there + * are 5 beans of type 'page' in the shared list. + * + * @param string $type type of bean you wish to count + * + * @return integer + */ + public function countShared( $type ) + { + $toolbox = $this->beanHelper->getToolbox(); + $redbean = $toolbox->getRedBean(); + $writer = $toolbox->getWriter(); + if ( $this->via ) { + $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) ); + if ( $oldName !== $this->via ) { + //set the new renaming rule + $writer->renameAssocTable( $oldName, $this->via ); + $this->via = NULL; + } + } + $type = $this->beau( $type ); + $count = 0; + if ( $this->getID() ) { + $count = $redbean->getAssociationManager()->relatedCount( $this, $type, $this->withSql, $this->withParams ); + } + $this->clearModifiers(); + return (integer) $count; + } + + /** + * Iterates through the specified own-list and + * fetches all properties (with their type) and + * returns the references. + * Use this method to quickly load indirectly related + * beans in an own-list. Whenever you cannot use a + * shared-list this method offers the same convenience + * by aggregating the parent beans of all children in + * the specified own-list. + * + * Example: + * + * + * $quest->aggr( 'xownQuestTarget', 'target', 'quest' ); + * + * + * Loads (in batch) and returns references to all + * quest beans residing in the $questTarget->target properties + * of each element in the xownQuestTargetList. + * + * @param string $list the list you wish to process + * @param string $property the property to load + * @param string $type the type of bean residing in this property (optional) + * + * @return array + */ + public function &aggr( $list, $property, $type = NULL ) + { + $this->via = NULL; + $ids = $beanIndex = $references = array(); + + if ( strlen( $list ) < 4 ) throw new RedException('Invalid own-list.'); + if ( strpos( $list, 'own') !== 0 ) throw new RedException('Only own-lists can be aggregated.'); + if ( !ctype_upper( substr( $list, 3, 1 ) ) ) throw new RedException('Invalid own-list.'); + + if ( is_null( $type ) ) $type = $property; + + foreach( $this->$list as $bean ) { + $field = $property . '_id'; + if ( isset( $bean->$field) ) { + $ids[] = $bean->$field; + $beanIndex[$bean->$field] = $bean; + } + } + + $beans = $this->beanHelper->getToolBox()->getRedBean()->batch( $type, $ids ); + + //now preload the beans as well + foreach( $beans as $bean ) { + $beanIndex[$bean->id]->setProperty( $property, $bean ); + } + + foreach( $beanIndex as $indexedBean ) { + $references[] = $indexedBean->$property; + } + + return $references; + } + + /** + * Tests whether the database identities of two beans are equal. + * Two beans are considered 'equal' if: + * + * a. the types of the beans match + * b. the ids of the beans match + * + * Returns TRUE if the beans are considered equal according to this + * specification and FALSE otherwise. + * + * Usage: + * + * + * $coffee->fetchAs( 'flavour' )->taste->equals( + * R::enum('flavour:mocca') + * ); + * + * + * The example above compares the flavour label 'mocca' with + * the flavour label attachec to the $coffee bean. This illustrates + * how to use equals() with RedBeanPHP-style enums. + * + * @param OODBBean $bean other bean + * + * @return boolean + */ + public function equals(OODBBean $bean) + { + return (bool) ( + ( (string) $this->properties['id'] === (string) $bean->properties['id'] ) + && ( (string) $this->__info['type'] === (string) $bean->__info['type'] ) + ); + } + + /** + * Magic method jsonSerialize, + * implementation for the \JsonSerializable interface, + * this method gets called by json_encode and + * facilitates a better JSON representation + * of the bean. Exports the bean on JSON serialization, + * for the JSON fans. + * + * Models can override jsonSerialize (issue #651) by + * implementing a __jsonSerialize method which should return + * an array. The __jsonSerialize override gets called with + * the @ modifier to prevent errors or warnings. + * + * @see http://php.net/manual/en/class.jsonserializable.php + * + * @return array + */ + public function jsonSerialize() + { + $json = $this->__call( '@__jsonSerialize', array( ) ); + + if ( $json !== NULL ) { + return $json; + } + + return $this->export(); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Observer as Observer; + +/** + * Observable + * Base class for Observables + * + * @file RedBeanPHP/Observable.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +abstract class Observable { //bracket must be here - otherwise coverage software does not understand. + + /** + * @var array + */ + private $observers = array(); + + /** + * Implementation of the Observer Pattern. + * Adds an event listener to the observable object. + * First argument should be the name of the event you wish to listen for. + * Second argument should be the object that wants to be notified in case + * the event occurs. + * + * @param string $eventname event identifier + * @param Observer $observer observer instance + * + * @return void + */ + public function addEventListener( $eventname, Observer $observer ) + { + if ( !isset( $this->observers[$eventname] ) ) { + $this->observers[$eventname] = array(); + } + + if ( in_array( $observer, $this->observers[$eventname] ) ) { + return; + } + + $this->observers[$eventname][] = $observer; + } + + /** + * Notifies listeners. + * Sends the signal $eventname, the event identifier and a message object + * to all observers that have been registered to receive notification for + * this event. Part of the observer pattern implementation in RedBeanPHP. + * + * @param string $eventname event you want signal + * @param mixed $info message object to send along + * + * @return void + */ + public function signal( $eventname, $info ) + { + if ( !isset( $this->observers[$eventname] ) ) { + $this->observers[$eventname] = array(); + } + + foreach ( $this->observers[$eventname] as $observer ) { + $observer->onEvent( $eventname, $info ); + } + } +} +} + +namespace RedBeanPHP { + +/** + * Observer. + * + * Interface for Observer object. Implementation of the + * observer pattern. + * + * @file RedBeanPHP/Observer.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * @desc Part of the observer pattern in RedBean + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Observer +{ + /** + * An observer object needs to be capable of receiving + * notifications. Therefore the observer needs to implement the + * onEvent method with two parameters: the event identifier specifying the + * current event and a message object (in RedBeanPHP this can also be a bean). + * + * @param string $eventname event identifier + * @param mixed $bean a message sent along with the notification + * + * @return void + */ + public function onEvent( $eventname, $bean ); +} +} + +namespace RedBeanPHP { + +/** + * Adapter Interface. + * Describes the API for a RedBeanPHP Database Adapter. + * This interface defines the API contract for + * a RedBeanPHP Database Adapter. + * + * @file RedBeanPHP/Adapter.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Adapter +{ + /** + * Should returns a string containing the most recent SQL query + * that has been processed by the adapter. + * + * @return string + */ + public function getSQL(); + + /** + * Executes an SQL Statement using an array of values to bind + * If $noevent is TRUE then this function will not signal its + * observers to notify about the SQL execution; this to prevent + * infinite recursion when using observers. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * @param boolean $noevent no event firing + * + * @return void + */ + public function exec( $sql, $bindings = array(), $noevent = FALSE ); + + /** + * Executes an SQL Query and returns a resultset. + * This method returns a multi dimensional resultset similar to getAll + * The values array can be used to bind values to the place holders in the + * SQL query. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return array + */ + public function get( $sql, $bindings = array() ); + + /** + * Executes an SQL Query and returns a resultset. + * This method returns a single row (one array) resultset. + * The values array can be used to bind values to the place holders in the + * SQL query. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return array + */ + public function getRow( $sql, $bindings = array() ); + + /** + * Executes an SQL Query and returns a resultset. + * This method returns a single column (one array) resultset. + * The values array can be used to bind values to the place holders in the + * SQL query. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return array + */ + public function getCol( $sql, $bindings = array() ); + + /** + * Executes an SQL Query and returns a resultset. + * This method returns a single cell, a scalar value as the resultset. + * The values array can be used to bind values to the place holders in the + * SQL query. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return string + */ + public function getCell( $sql, $bindings = array() ); + + /** + * Executes the SQL query specified in $sql and indexes + * the row by the first column. + * + * @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 returns + * an associative array where the column names are the keys. + * + * @param string $sql Sstring containing SQL code for databaseQL + * @param array $bindings values to bind + * + * @return array + */ + public function getAssocRow( $sql, $bindings = array() ); + + /** + * Returns the latest insert ID. + * + * @return integer + */ + public function getInsertID(); + + /** + * Returns the number of rows that have been + * affected by the last update statement. + * + * @return integer + */ + public function getAffectedRows(); + + /** + * Returns a database agnostic Cursor object. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return Cursor + */ + public function getCursor( $sql, $bindings = array() ); + + /** + * Returns the original database resource. This is useful if you want to + * perform operations on the driver directly instead of working with the + * adapter. RedBean will only access the adapter and never to talk + * directly to the driver though. + * + * @return mixed + */ + public function getDatabase(); + + /** + * This method is part of the RedBean Transaction Management + * mechanisms. + * Starts a transaction. + * + * @return void + */ + public function startTransaction(); + + /** + * This method is part of the RedBean Transaction Management + * mechanisms. + * Commits the transaction. + * + * @return void + */ + public function commit(); + + /** + * This method is part of the RedBean Transaction Management + * mechanisms. + * Rolls back the transaction. + * + * @return void + */ + public function rollback(); + + /** + * Closes database connection. + * + * @return void + */ + public function close(); + + /** + * Sets a driver specific option. + * Using this method you can access driver-specific functions. + * If the selected option exists the value will be passed and + * this method will return boolean TRUE, otherwise it will return + * boolean FALSE. + * + * @param string $optionKey option key + * @param string $optionValue option value + * + * @return boolean + */ + public function setOption( $optionKey, $optionValue ); + + /** + * Returns the version string from the database server. + * + * @return string + */ + public function getDatabaseServerVersion(); +} +} + +namespace RedBeanPHP\Adapter { + +use RedBeanPHP\Observable as Observable; +use RedBeanPHP\Adapter as Adapter; +use RedBeanPHP\Driver as Driver; + +/** + * DBAdapter (Database Adapter) + * + * An adapter class to connect various database systems to RedBean + * Database Adapter Class. The task of the database adapter class is to + * communicate with the database driver. You can use all sorts of database + * drivers with RedBeanPHP. The default database drivers that ships with + * the RedBeanPHP library is the RPDO driver ( which uses the PHP Data Objects + * Architecture aka PDO ). + * + * @file RedBeanPHP/Adapter/DBAdapter.php + * @author Gabor de Mooij and the RedBeanPHP Community. + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class DBAdapter extends Observable implements Adapter +{ + /** + * @var Driver + */ + private $db = NULL; + + /** + * @var string + */ + private $sql = ''; + + /** + * Constructor. + * + * Creates an instance of the RedBean Adapter Class. + * This class provides an interface for RedBean to work + * with ADO compatible DB instances. + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Driver $database ADO Compatible DB Instance + */ + public function __construct( $database ) + { + $this->db = $database; + } + + /** + * Returns a string containing the most recent SQL query + * processed by the database adapter, thus conforming to the + * interface: + * + * @see Adapter::getSQL + * + * Methods like get(), getRow() and exec() cause this SQL cache + * to get filled. If no SQL query has been processed yet this function + * will return an empty string. + * + * @return string + */ + public function getSQL() + { + return $this->sql; + } + + /** + * @see Adapter::exec + */ + public function exec( $sql, $bindings = array(), $noevent = FALSE ) + { + if ( !$noevent ) { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + } + + return $this->db->Execute( $sql, $bindings ); + } + + /** + * @see Adapter::get + */ + public function get( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetAll( $sql, $bindings ); + } + + /** + * @see Adapter::getRow + */ + public function getRow( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetRow( $sql, $bindings ); + } + + /** + * @see Adapter::getCol + */ + public function getCol( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetCol( $sql, $bindings ); + } + + /** + * @see Adapter::getAssoc + */ + public function getAssoc( $sql, $bindings = array() ) + { + $this->sql = $sql; + + $this->signal( 'sql_exec', $this ); + + $rows = $this->db->GetAll( $sql, $bindings ); + + if ( !$rows ) return array(); + + $assoc = array(); + + foreach ( $rows as $row ) { + if ( empty( $row ) ) continue; + + $key = array_shift( $row ); + switch ( count( $row ) ) { + case 0: + $value = $key; + break; + case 1: + $value = reset( $row ); + break; + default: + $value = $row; + } + + $assoc[$key] = $value; + } + + return $assoc; + } + + /** + * @see Adapter::getAssocRow + */ + public function getAssocRow($sql, $bindings = array()) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetAssocRow( $sql, $bindings ); + } + + /** + * @see Adapter::getCell + */ + public function getCell( $sql, $bindings = array(), $noSignal = NULL ) + { + $this->sql = $sql; + + if ( !$noSignal ) $this->signal( 'sql_exec', $this ); + + return $this->db->GetOne( $sql, $bindings ); + } + + /** + * @see Adapter::getCursor + */ + public function getCursor( $sql, $bindings = array() ) + { + return $this->db->GetCursor( $sql, $bindings ); + } + + /** + * @see Adapter::getInsertID + */ + public function getInsertID() + { + return $this->db->getInsertID(); + } + + /** + * @see Adapter::getAffectedRows + */ + public function getAffectedRows() + { + return $this->db->Affected_Rows(); + } + + /** + * @see Adapter::getDatabase + */ + public function getDatabase() + { + return $this->db; + } + + /** + * @see Adapter::startTransaction + */ + public function startTransaction() + { + $this->db->StartTrans(); + } + + /** + * @see Adapter::commit + */ + public function commit() + { + $this->db->CommitTrans(); + } + + /** + * @see Adapter::rollback + */ + public function rollback() + { + $this->db->FailTrans(); + } + + /** + * @see Adapter::close. + */ + public function close() + { + $this->db->close(); + } + + /** + * Sets initialization code for connection. + * + * @param callable $code + */ + public function setInitCode($code) { + $this->db->setInitCode($code); + } + + /** + * @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; + } + + /** + * @see Adapter::getDatabaseServerVersion + */ + public function getDatabaseServerVersion() + { + return $this->db->DatabaseServerVersion(); + } +} +} + +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(); + + /** + * Resets the cursor by closing it and re-executing the statement. + * This reloads fresh data from the database for the whole collection. + * + * @return void + */ + public function reset(); + + /** + * 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::reset + */ + public function reset() + { + $this->close(); + $this->res->execute(); + } + + /** + * @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::reset + */ + public function reset() + { + 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 ) ); + return reset( $beans ); + } + return NULL; + } + + /** + * Resets the collection from the start, like a fresh() on a bean. + * + * @return void + */ + public function reset() + { + $this->cursor->reset(); + } + + /** + * Closes the underlying cursor (needed for some databases). + * + * @return void + */ + public function close() + { + $this->cursor->close(); + } +} +} + +namespace RedBeanPHP { + +/** + * QueryWriter + * Interface for QueryWriters. + * Describes the API for a QueryWriter. + * + * Terminology: + * + * - beautified property (a camelCased property, has to be converted first) + * - beautified type (a camelCased type, has to be converted first) + * - type (a bean type, corresponds directly to a table) + * - property (a bean property, corresponds directly to a column) + * - table (a checked and quoted type, ready for use in a query) + * - column (a checked and quoted property, ready for use in query) + * - tableNoQ (same as type, but in context of a database operation) + * - columnNoQ (same as property, but in context of a database operation) + * + * @file RedBeanPHP/QueryWriter.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface QueryWriter +{ + /** + * SQL filter constants + */ + const C_SQLFILTER_READ = 'r'; + const C_SQLFILTER_WRITE = 'w'; + + /** + * Query Writer constants. + */ + const C_SQLSTATE_NO_SUCH_TABLE = 1; + const C_SQLSTATE_NO_SUCH_COLUMN = 2; + const C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION = 3; + const C_SQLSTATE_LOCK_TIMEOUT = 4; + + /** + * Define data type regions + * + * 00 - 80: normal data types + * 80 - 99: special data types, only scan/code if requested + * 99 : specified by user, don't change + */ + const C_DATATYPE_RANGE_SPECIAL = 80; + const C_DATATYPE_RANGE_SPECIFIED = 99; + + /** + * Define GLUE types for use with glueSQLCondition methods. + * Determines how to prefix a snippet of SQL before appending it + * to other SQL (or integrating it, mixing it otherwise). + * + * WHERE - glue as WHERE condition + * AND - glue as AND condition + */ + const C_GLUE_WHERE = 1; + const C_GLUE_AND = 2; + + /** + * CTE Select Snippet + * Constants specifying select snippets for CTE queries + */ + const C_CTE_SELECT_NORMAL = FALSE; + const C_CTE_SELECT_COUNT = TRUE; + + /** + * Parses an sql string to create joins if needed. + * + * For instance with $type = 'book' and $sql = ' @joined.author.name LIKE ? OR @joined.detail.title LIKE ? ' + * parseJoin will return the following SQL: + * ' LEFT JOIN `author` ON `author`.id = `book`.author_id + * LEFT JOIN `detail` ON `detail`.id = `book`.detail_id + * WHERE author.name LIKE ? OR detail.title LIKE ? ' + * + * @note this feature requires Narrow Field Mode to be activated (default). + * + * @note A default implementation is available in AQueryWriter + * unless a database uses very different SQL this should suffice. + * + * @param string $type the source type for the join + * @param string $sql the sql string to be parsed + * + * @return string + */ + public function parseJoin( $type, $sql ); + + /** + * 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 relation between joined tables (possible: 'parent', 'own', 'shared') + * @param boolean $firstOfChain is it the join of a chain (or the only join) + * @param string $suffix suffix to add for aliasing tables (for joining same table multiple times) + * + * @return string $joinSQLSnippet + */ + public function writeJoin( $type, $targetType, $leftRight, $joinType, $firstOfChain, $suffix ); + + /** + * Glues an SQL snippet to the beginning of a WHERE clause. + * This ensures users don't have to add WHERE to their query snippets. + * + * The snippet gets prefixed with WHERE or AND + * if it starts with a condition. + * + * If the snippet does NOT start with a condition (or this function thinks so) + * the snippet is returned as-is. + * + * The GLUE type determines the prefix: + * + * * NONE prefixes with WHERE + * * WHERE prefixes with WHERE and replaces AND if snippets starts with AND + * * AND prefixes with AND + * + * This method will never replace WHERE with AND since a snippet should never + * begin with WHERE in the first place. OR is not supported. + * + * Only a limited set of clauses will be recognized as non-conditions. + * For instance beginning a snippet with complex statements like JOIN or UNION + * will not work. This is too complex for use in a snippet. + * + * @note A default implementation is available in AQueryWriter + * unless a database uses very different SQL this should suffice. + * + * @param string $sql SQL Snippet + * @param integer $glue the GLUE type - how to glue (C_GLUE_WHERE or C_GLUE_AND) + * + * @return string + */ + public function glueSQLCondition( $sql, $glue = NULL ); + + /** + * Determines if there is a LIMIT 1 clause in the SQL. + * If not, it will add a LIMIT 1. (used for findOne). + * + * @note A default implementation is available in AQueryWriter + * unless a database uses very different SQL this should suffice. + * + * @param string $sql query to scan and adjust + * + * @return string + */ + public function glueLimitOne( $sql ); + + /** + * Returns the tables that are in the database. + * + * @return array + */ + public function getTables(); + + /** + * This method will create a table for the bean. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type of bean you want to create a table for + * + * @return void + */ + public function createTable( $type ); + + /** + * Returns an array containing all the columns of the specified type. + * The format of the return array looks like this: + * $field => $type where $field is the name of the column and $type + * is a database specific description of the datatype. + * + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type of bean you want to obtain a column list of + * + * @return array + */ + public function getColumns( $type ); + + /** + * Returns the Column Type Code (integer) that corresponds + * to the given value type. This method is used to determine the minimum + * column type required to represent the given value. There are two modes of + * operation: with or without special types. Scanning without special types + * requires the second parameter to be set to FALSE. This is useful when the + * column has already been created and prevents it from being modified to + * an incompatible type leading to data loss. Special types will be taken + * into account when a column does not exist yet (parameter is then set to TRUE). + * + * Special column types are determines by the AQueryWriter constant + * C_DATA_TYPE_ONLY_IF_NOT_EXISTS (usually 80). Another 'very special' type is type + * C_DATA_TYPE_MANUAL (usually 99) which represents a user specified type. Although + * no special treatment has been associated with the latter for now. + * + * @param string $value value + * @param boolean $alsoScanSpecialForTypes take special types into account + * + * @return integer + */ + public function scanType( $value, $alsoScanSpecialForTypes = FALSE ); + + /** + * This method will add a column to a table. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type name of the table + * @param string $column name of the column + * @param integer $field data type for field + * + * @return void + */ + public function addColumn( $type, $column, $field ); + + /** + * Returns the Type Code for a Column Description. + * Given an SQL column description this method will return the corresponding + * code for the writer. If the include specials flag is set it will also + * return codes for special columns. Otherwise special columns will be identified + * as specified columns. + * + * @param string $typedescription description + * @param boolean $includeSpecials whether you want to get codes for special columns as well + * + * @return integer + */ + public function code( $typedescription, $includeSpecials = FALSE ); + + /** + * This method will widen the column to the specified data type. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type / table that needs to be adjusted + * @param string $column column that needs to be altered + * @param integer $datatype target data type + * + * @return void + */ + public function widenColumn( $type, $column, $datatype ); + + /** + * Selects records from the database. + * This methods selects the records from the database that match the specified + * type, conditions (optional) and additional SQL snippet (optional). + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSql additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return array + */ + public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ); + + /** + * Selects records from the database and returns a cursor. + * This methods selects the records from the database that match the specified + * type, conditions (optional) and additional SQL snippet (optional). + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return Cursor + */ + public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() ); + + /** + * Returns records through an intermediate type. This method is used to obtain records using a link table and + * allows the SQL snippets to reference columns in the link table for additional filtering or ordering. + * + * @param string $sourceType source type, the reference type you want to use to fetch related items on the other side + * @param string $destType destination type, the target type you want to get beans of + * @param mixed $linkID ID to use for the link table + * @param string $addSql Additional SQL snippet + * @param array $bindings Bindings for SQL snippet + * + * @return array + */ + public function queryRecordRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() ); + + /** + * Returns the row that links $sourceType $sourcID to $destType $destID in an N-M relation. + * + * @param string $sourceType source type, the first part of the link you're looking for + * @param string $destType destination type, the second part of the link you're looking for + * @param string $sourceID ID for the source + * @param string $destID ID for the destination + * + * @return array|null + */ + public function queryRecordLink( $sourceType, $destType, $sourceID, $destID ); + + /** + * Counts the number of records in the database that match the + * conditions and additional SQL. + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return integer + */ + public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() ); + + /** + * Returns the number of records linked through $linkType and satisfying the SQL in $addSQL/$bindings. + * + * @param string $sourceType source type + * @param string $targetType the thing you want to count + * @param mixed $linkID the of the source type + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return integer + */ + public function queryRecordCountRelated( $sourceType, $targetType, $linkID, $addSQL = '', $bindings = array() ); + + /** + * Returns all rows of specified type that have been tagged with one of the + * strings in the specified tag list array. + * + * Note that the additional SQL snippet can only be used for pagination, + * the SQL snippet will be appended to the end of the query. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * + * @return array + */ + public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ); + + /** + * Like queryTagged but only counts. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * + * @return integer + */ + public function queryCountTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ); + + /** + * Returns all parent rows or child rows of a specified row. + * Given a type specifier and a primary key id, this method returns either all child rows + * as defined by having _id = id or all parent rows as defined per id = _id + * taking into account an optional SQL snippet along with parameters. + * + * The $select parameter can be used to adjust the select snippet of the query. + * Possible values are: C_CTE_SELECT_NORMAL (just select all columns, default), C_CTE_SELECT_COUNT + * (count rows) used for countParents and countChildren functions - or you can specify a + * string yourself like 'count(distinct brand)'. + * + * @param string $type the bean type you want to query rows for + * @param integer $id id of the reference row + * @param boolean $up TRUE to query parent rows, FALSE to query child rows + * @param string $addSql optional SQL snippet to embed in the query + * @param array $bindings parameter bindings for additional SQL snippet + * @param mixed $select Select Snippet to use when querying (optional) + * + * @return array + */ + public function queryRecursiveCommonTableExpression( $type, $id, $up = TRUE, $addSql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_NORMAL ); + + /** + * This method should update (or insert a record), it takes + * a table name, a list of update values ( $field => $value ) and an + * primary key ID (optional). If no primary key ID is provided, an + * INSERT will take place. + * Returns the new ID. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type name of the table to update + * @param array $updatevalues list of update values + * @param integer $id optional primary key ID value + * + * @return integer + */ + public function updateRecord( $type, $updatevalues, $id = NULL ); + + /** + * Deletes records from the database. + * @note $addSql is always prefixed with ' WHERE ' or ' AND .' + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSql additional SQL + * @param array $bindings bindings + * + * @return void + */ + public function deleteRecord( $type, $conditions = array(), $addSql = '', $bindings = array() ); + + /** + * Deletes all links between $sourceType and $destType in an N-M relation. + * + * @param string $sourceType source type + * @param string $destType destination type + * @param string $sourceID source ID + * + * @return void + */ + public function deleteRelations( $sourceType, $destType, $sourceID ); + + /** + * @see QueryWriter::addUniqueConstaint + */ + public function addUniqueIndex( $type, $columns ); + + /** + * This method will add a UNIQUE constraint index to a table on columns $columns. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type target bean type + * @param array $columnsPartOfIndex columns to include in index + * + * @return void + */ + public function addUniqueConstraint( $type, $columns ); + + /** + * This method will check whether the SQL state is in the list of specified states + * and returns TRUE if it does appear in this list or FALSE if it + * does not. The purpose of this method is to translate the database specific state to + * a one of the constants defined in this class and then check whether it is in the list + * of standard states provided. + * + * @param string $state SQL state to consider + * @param array $list list of standardized SQL state constants to check against + * @param array $extraDriverDetails Some databases communicate state information in a driver-specific format + * rather than through the main sqlState code. For those databases, this extra + * information can be used to determine the standardized state + * + * @return boolean + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ); + + /** + * This method will remove all beans of a certain type. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type bean type + * + * @return void + */ + public function wipe( $type ); + + /** + * This method will add a foreign key from type and field to + * target type and target field. + * The foreign key is created without an action. On delete/update + * no action will be triggered. The FK is only used to allow database + * tools to generate pretty diagrams and to make it easy to add actions + * later on. + * This methods accepts a type and infers the corresponding table name. + * + * + * @param string $type type that will have a foreign key field + * @param string $targetType points to this type + * @param string $property field that contains the foreign key value + * @param string $targetProperty field where the fk points to + * @param string $isDep whether target is dependent and should cascade on update/delete + * + * @return void + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ); + + /** + * This method will add an index to a type and field with name + * $name. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type to add index to + * @param string $name name of the new index + * @param string $property field to index + * + * @return void + */ + public function addIndex( $type, $name, $property ); + + /** + * Checks and filters a database structure element like a table of column + * for safe use in a query. A database structure has to conform to the + * RedBeanPHP DB security policy which basically means only alphanumeric + * symbols are allowed. This security policy is more strict than conventional + * SQL policies and does therefore not require database specific escaping rules. + * + * @param string $databaseStructure name of the column/table to check + * @param boolean $noQuotes TRUE to NOT put backticks or quotes around the string + * + * @return string + */ + public function esc( $databaseStructure, $dontQuote = FALSE ); + + /** + * Removes all tables and views from the database. + * + * @return void + */ + public function wipeAll(); + + /** + * Renames an association. For instance if you would like to refer to + * album_song as: track you can specify this by calling this method like: + * + * + * renameAssociation('album_song','track') + * + * + * This allows: + * + * + * $album->sharedSong + * + * + * to add/retrieve beans from track instead of album_song. + * Also works for exportAll(). + * + * This method also accepts a single associative array as + * its first argument. + * + * @param string|array $fromType original type name, or array + * @param string $toType new type name (only if 1st argument is string) + * + * @return void + */ + public function renameAssocTable( $fromType, $toType = NULL ); + + /** + * Returns the format for link tables. + * Given an array containing two type names this method returns the + * name of the link table to be used to store and retrieve + * association records. For instance, given two types: person and + * project, the corresponding link table might be: 'person_project'. + * + * @param array $types two types array($type1, $type2) + * + * @return string + */ + public function getAssocTable( $types ); + +} +} + +namespace RedBeanPHP\QueryWriter { + +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\RedException\SQL as SQLException; + +/** + * RedBeanPHP Abstract Query Writer. + * Represents an abstract Database to RedBean + * To write a driver for a different database for RedBean + * Contains a number of functions all implementors can + * inherit or override. + * + * @file RedBeanPHP/QueryWriter/AQueryWriter.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +abstract class AQueryWriter +{ + /** + * Constant: Select Snippet 'FOR UPDATE' + */ + const C_SELECT_SNIPPET_FOR_UPDATE = 'FOR UPDATE'; + const C_DATA_TYPE_ONLY_IF_NOT_EXISTS = 80; + const C_DATA_TYPE_MANUAL = 99; + + /** + * @var array + */ + private static $sqlFilters = array(); + + /** + * @var boolean + */ + private static $flagSQLFilterSafeMode = FALSE; + + /** + * @var boolean + */ + private static $flagNarrowFieldMode = TRUE; + + /** + * @var boolean + */ + protected static $flagUseJSONColumns = FALSE; + + /** + * @var boolean + */ + protected static $enableISNULLConditions = FALSE; + + /** + * @var array + */ + public static $renames = array(); + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * @var string + */ + protected $defaultValue = 'NULL'; + + /** + * @var string + */ + protected $quoteCharacter = ''; + + /** + * @var boolean + */ + protected $flagUseCache = TRUE; + + /** + * @var array + */ + protected $cache = array(); + + /** + * @var integer + */ + protected $maxCacheSizePerType = 20; + + /** + * @var string + */ + protected $sqlSelectSnippet = ''; + + /** + * @var array + */ + public $typeno_sqltype = array(); + + /** + * @var bool + */ + protected static $noNuke = false; + + /** + * Sets a data definition template to change the data + * creation statements per type. + * + * For instance to add ROW_FORMAT=DYNAMIC to all MySQL tables + * upon creation: + * + * $sql = $writer->getDDLTemplate( 'createTable', '*' ); + * $writer->setDDLTemplate( 'createTable', '*', $sql . ' ROW_FORMAT=DYNAMIC ' ); + * + * For property-specific templates set $beanType to: + * account.username -- then the template will only be applied to SQL statements relating + * to that column/property. + * + * @param string $type ( 'createTable' | 'widenColumn' | 'addColumn' ) + * @param string $beanType ( type of bean or '*' to apply to all types ) + * @param string $template SQL template, contains %s for slots + * + * @return void + */ + public function setDDLTemplate( $type, $beanType, $template ) + { + $this->DDLTemplates[ $type ][ $beanType ] = $template; + } + + /** + * Returns the specified data definition template. + * If no template can be found for the specified type, the template for + * '*' will be returned instead. + * + * @param string $type ( 'createTable' | 'widenColumn' | 'addColumn' ) + * @param string $beanType ( type of bean or '*' to apply to all types ) + * @param string $property specify if you're looking for a property-specific template + * + * @return string + */ + public function getDDLTemplate( $type, $beanType = '*', $property = NULL ) + { + $key = ( $property ) ? "{$beanType}.{$property}" : $beanType; + if ( isset( $this->DDLTemplates[ $type ][ $key ] ) ) { + return $this->DDLTemplates[ $type ][ $key ]; + } + if ( isset( $this->DDLTemplates[ $type ][ $beanType ] ) ) { + return $this->DDLTemplates[ $type ][ $beanType ]; + } + return $this->DDLTemplates[ $type ][ '*' ]; + } + + /** + * Toggles support for IS-NULL-conditions. + * If IS-NULL-conditions are enabled condition arrays + * for functions including findLike() are treated so that + * 'field' => NULL will be interpreted as field IS NULL + * instead of being skipped. Returns the previous + * value of the flag. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function useISNULLConditions( $flag ) + { + $old = self::$enableISNULLConditions; + self::$enableISNULLConditions = $flag; + return $old; + } + + /** + * 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; + } + + /** + * Toggles support for nuke(). + * Can be used to turn off the nuke() feature for security reasons. + * Returns the old flag value. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function forbidNuke( $flag ) { + $old = self::$noNuke; + self::$noNuke = (bool) $flag; + return $old; + } + + /** + * Checks whether a number can be treated like an int. + * + * @param string $value string representation of a certain value + * + * @return boolean + */ + public static function canBeTreatedAsInt( $value ) + { + return (bool) ( strval( $value ) === strval( intval( $value ) ) ); + } + + /** + * @see QueryWriter::getAssocTableFormat + */ + public static function getAssocTableFormat( $types ) + { + sort( $types ); + + $assoc = implode( '_', $types ); + + return ( isset( self::$renames[$assoc] ) ) ? self::$renames[$assoc] : $assoc; + } + + /** + * @see QueryWriter::renameAssociation + */ + public static function renameAssociation( $from, $to = NULL ) + { + if ( is_array( $from ) ) { + foreach ( $from as $key => $value ) self::$renames[$key] = $value; + + return; + } + + self::$renames[$from] = $to; + } + + /** + * Globally available service method for RedBeanPHP. + * Converts a camel cased string to a snake cased string. + * + * @param string $camel camelCased string to converty to snake case + * + * @return string + */ + public static function camelsSnake( $camel ) + { + return strtolower( preg_replace( '/(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])/', '_$1$2', $camel ) ); + } + + /** + * Clears renames. + * + * @return void + */ + public static function clearRenames() + { + self::$renames = array(); + } + + /** + * Toggles 'Narrow Field Mode'. + * In Narrow Field mode the queryRecord method will + * narrow its selection field to + * + * SELECT {table}.* + * + * instead of + * + * SELECT * + * + * This is a better way of querying because it allows + * more flexibility (for instance joins). However if you need + * the wide selector for backward compatibility; use this method + * to turn OFF Narrow Field Mode by passing FALSE. + * Default is TRUE. + * + * @param boolean $narrowField TRUE = Narrow Field FALSE = Wide Field + * + * @return void + */ + public static function setNarrowFieldMode( $narrowField ) + { + self::$flagNarrowFieldMode = (boolean) $narrowField; + } + + /** + * Sets SQL filters. + * This is a lowlevel method to set the SQL filter array. + * The format of this array is: + * + * + * array( + * '' => array( + * '' => array( + * '' => '' + * ) + * ) + * ) + * + * + * Example: + * + * + * array( + * QueryWriter::C_SQLFILTER_READ => array( + * 'book' => array( + * 'title' => ' LOWER(book.title) ' + * ) + * ) + * + * + * Note that you can use constants instead of magical chars + * as keys for the uppermost array. + * This is a lowlevel method. For a more friendly method + * please take a look at the facade: R::bindFunc(). + * + * @param array list of filters to set + * + * @return void + */ + public static function setSQLFilters( $sqlFilters, $safeMode = FALSE ) + { + self::$flagSQLFilterSafeMode = (boolean) $safeMode; + self::$sqlFilters = $sqlFilters; + } + + /** + * Returns current SQL Filters. + * This method returns the raw SQL filter array. + * This is a lowlevel method. For a more friendly method + * please take a look at the facade: R::bindFunc(). + * + * @return array + */ + public static function getSQLFilters() + { + return self::$sqlFilters; + } + + /** + * Returns a cache key for the cache values passed. + * This method returns a fingerprint string to be used as a key to store + * data in the writer cache. + * + * @param array $keyValues key-value to generate key for + * + * @return string + */ + private function getCacheKey( $keyValues ) + { + return json_encode( $keyValues ); + } + + /** + * Returns the values associated with the provided cache tag and key. + * + * @param string $cacheTag cache tag to use for lookup + * @param string $key key to use for lookup + * + * @return mixed + */ + private function getCached( $cacheTag, $key ) + { + $sql = $this->adapter->getSQL(); + if ($this->updateCache()) { + if ( isset( $this->cache[$cacheTag][$key] ) ) { + return $this->cache[$cacheTag][$key]; + } + } + + return NULL; + } + + /** + * Checks if the previous query had a keep-cache tag. + * If so, the cache will persist, otherwise the cache will be flushed. + * + * Returns TRUE if the cache will remain and FALSE if a flush has + * been performed. + * + * @return boolean + */ + private function updateCache() + { + $sql = $this->adapter->getSQL(); + if ( strpos( $sql, '-- keep-cache' ) !== strlen( $sql ) - 13 ) { + // If SQL has been taken place outside of this method then something else then + // a select query might have happened! (or instruct to keep cache) + $this->cache = array(); + return FALSE; + } + return TRUE; + } + + /** + * Stores data from the writer in the cache under a specific key and cache tag. + * A cache tag is used to make sure the cache remains consistent. In most cases the cache tag + * will be the bean type, this makes sure queries associated with a certain reference type will + * never contain conflicting data. + * Why not use the cache tag as a key? Well + * we need to make sure the cache contents fits the key (and key is based on the cache values). + * Otherwise it would be possible to store two different result sets under the same key (the cache tag). + * + * In previous versions you could only store one key-entry, I have changed this to + * improve caching efficiency (issue #400). + * + * @param string $cacheTag cache tag (secondary key) + * @param string $key key to store values under + * @param array $values content to be stored + * + * @return void + */ + private function putResultInCache( $cacheTag, $key, $values ) + { + if ( isset( $this->cache[$cacheTag] ) ) { + if ( count( $this->cache[$cacheTag] ) > $this->maxCacheSizePerType ) array_shift( $this->cache[$cacheTag] ); + } else { + $this->cache[$cacheTag] = array(); + } + $this->cache[$cacheTag][$key] = $values; + } + + /** + * Creates an SQL snippet from a list of conditions of format: + * + * + * array( + * key => array( + * value1, value2, value3 .... + * ) + * ) + * + * + * @param array $conditions list of conditions + * @param array $bindings parameter bindings for SQL snippet + * @param string $addSql additional SQL snippet to append to result + * + * @return string + */ + private function makeSQLFromConditions( $conditions, &$bindings, $addSql = '' ) + { + reset( $bindings ); + $firstKey = key( $bindings ); + $paramTypeIsNum = ( is_numeric( $firstKey ) ); + $counter = 0; + + $sqlConditions = array(); + foreach ( $conditions as $column => $values ) { + if ( $values === NULL ) { + if ( self::$enableISNULLConditions ) { + $sqlConditions[] = $this->esc( $column ) . ' IS NULL'; + } + continue; + } + + if ( is_array( $values ) ) { + if ( empty( $values ) ) continue; + } else { + $values = array( $values ); + } + + $checkOODB = reset( $values ); + if ( $checkOODB instanceof OODBBean && $checkOODB->getMeta( 'type' ) === $column && substr( $column, -3 ) != '_id' ) + $column = $column . '_id'; + + + $sql = $this->esc( $column ); + $sql .= ' IN ( '; + + if ( $paramTypeIsNum ) { + $sql .= implode( ',', array_fill( 0, count( $values ), '?' ) ) . ' ) '; + + array_unshift($sqlConditions, $sql); + + foreach ( $values as $k => $v ) { + if ( $v instanceof OODBBean ) { + $v = $v->id; + } + $values[$k] = strval( $v ); + + array_unshift( $bindings, $v ); + } + } else { + + $slots = array(); + + foreach( $values as $k => $v ) { + if ( $v instanceof OODBBean ) { + $v = $v->id; + } + $slot = ':slot'.$counter++; + $slots[] = $slot; + $bindings[$slot] = strval( $v ); + } + + $sql .= implode( ',', $slots ).' ) '; + $sqlConditions[] = $sql; + } + } + + $sql = ''; + if ( !empty( $sqlConditions ) ) { + $sql .= " WHERE ( " . implode( ' AND ', $sqlConditions ) . ") "; + } + + $addSql = $this->glueSQLCondition( $addSql, !empty( $sqlConditions ) ? QueryWriter::C_GLUE_AND : NULL ); + if ( $addSql ) $sql .= $addSql; + + return $sql; + } + + /** + * Returns the table names and column names for a relational query. + * + * @param string $sourceType type of the source bean + * @param string $destType type of the bean you want to obtain using the relation + * @param boolean $noQuote TRUE if you want to omit quotes + * + * @return array + */ + private function getRelationalTablesAndColumns( $sourceType, $destType, $noQuote = FALSE ) + { + $linkTable = $this->esc( $this->getAssocTable( array( $sourceType, $destType ) ), $noQuote ); + $sourceCol = $this->esc( $sourceType . '_id', $noQuote ); + + if ( $sourceType === $destType ) { + $destCol = $this->esc( $destType . '2_id', $noQuote ); + } else { + $destCol = $this->esc( $destType . '_id', $noQuote ); + } + + $sourceTable = $this->esc( $sourceType, $noQuote ); + $destTable = $this->esc( $destType, $noQuote ); + + return array( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ); + } + + /** + * Determines whether a string can be considered JSON or not. + * This is used by writers that support JSON columns. However + * we dont want that code duplicated over all JSON supporting + * Query Writers. + * + * @param string $value value to determine 'JSONness' of. + * + * @return boolean + */ + protected function isJSON( $value ) + { + return ( + is_string($value) && + is_array(json_decode($value, TRUE)) && + (json_last_error() == JSON_ERROR_NONE) + ); + } + + /** + * Given a type and a property name this method + * returns the foreign key map section associated with this pair. + * + * @param string $type name of the type + * @param string $property name of the property + * + * @return array|NULL + */ + protected function getForeignKeyForTypeProperty( $type, $property ) + { + $property = $this->esc( $property, TRUE ); + + try { + $map = $this->getKeyMapForType( $type ); + } catch ( SQLException $e ) { + return NULL; + } + + foreach( $map as $key ) { + if ( $key['from'] === $property ) return $key; + } + return NULL; + } + + /** + * Returns the foreign key map (FKM) for a type. + * A foreign key map describes the foreign keys in a table. + * A FKM always has the same structure: + * + * + * array( + * 'name' => + * 'from' => + * 'table' => + * 'to' => (most of the time 'id') + * 'on_update' => + * 'on_delete' => + * ) + * + * + * @note the keys in the result array are FKDLs, i.e. descriptive unique + * keys per source table. Also see: AQueryWriter::makeFKLabel for details. + * + * @param string $type the bean type you wish to obtain a key map of + * + * @return array + */ + protected function getKeyMapForType( $type ) + { + return array(); + } + + /** + * This method makes a key for a foreign key description array. + * This key is a readable string unique for every source table. + * This uniform key is called the FKDL Foreign Key Description Label. + * Note that the source table is not part of the FKDL because + * this key is supposed to be 'per source table'. If you wish to + * include a source table, prefix the key with 'on_table__'. + * + * @param string $from the column of the key in the source table + * @param string $type the type (table) where the key points to + * @param string $to the target column of the foreign key (mostly just 'id') + * + * @return string + */ + protected function makeFKLabel($from, $type, $to) + { + return "from_{$from}_to_table_{$type}_col_{$to}"; + } + + /** + * Returns an SQL Filter snippet for reading. + * + * @param string $type type of bean + * + * @return string + */ + protected function getSQLFilterSnippet( $type ) + { + $existingCols = array(); + if (self::$flagSQLFilterSafeMode) { + $existingCols = $this->getColumns( $type ); + } + + $sqlFilters = array(); + if ( isset( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] ) ) { + foreach( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] as $property => $sqlFilter ) { + if ( !self::$flagSQLFilterSafeMode || isset( $existingCols[$property] ) ) { + $sqlFilters[] = $sqlFilter.' AS '.$property.' '; + } + } + } + $sqlFilterStr = ( count($sqlFilters) ) ? ( ','.implode( ',', $sqlFilters ) ) : ''; + return $sqlFilterStr; + } + + /** + * Generates a list of parameters (slots) for an SQL snippet. + * This method calculates the correct number of slots to insert in the + * SQL snippet and determines the correct type of slot. If the bindings + * array contains named parameters this method will return named ones and + * update the keys in the value list accordingly (that's why we use the &). + * + * If you pass an offset the bindings will be re-added to the value list. + * Some databases cant handle duplicate parameter names in queries. + * + * @param array &$valueList list of values to generate slots for (gets modified if needed) + * @param array $otherBindings list of additional bindings + * @param integer $offset start counter at... + * + * @return string + */ + protected function getParametersForInClause( &$valueList, $otherBindings, $offset = 0 ) + { + if ( is_array( $otherBindings ) && count( $otherBindings ) > 0 ) { + reset( $otherBindings ); + + $key = key( $otherBindings ); + + if ( !is_numeric($key) ) { + $filler = array(); + $newList = (!$offset) ? array() : $valueList; + $counter = $offset; + + foreach( $valueList as $value ) { + $slot = ':slot' . ( $counter++ ); + $filler[] = $slot; + $newList[$slot] = $value; + } + + // Change the keys! + $valueList = $newList; + + return implode( ',', $filler ); + } + } + + return implode( ',', array_fill( 0, count( $valueList ), '?' ) ); + } + + /** + * Adds a data type to the list of data types. + * Use this method to add a new column type definition to the writer. + * Used for UUID support. + * + * @param integer $dataTypeID magic number constant assigned to this data type + * @param string $SQLDefinition SQL column definition (i.e. INT(11)) + * + * @return self + */ + protected function addDataType( $dataTypeID, $SQLDefinition ) + { + $this->typeno_sqltype[ $dataTypeID ] = $SQLDefinition; + $this->sqltype_typeno[ $SQLDefinition ] = $dataTypeID; + return $this; + } + + /** + * Returns the sql that should follow an insert statement. + * + * @param string $table name + * + * @return string + */ + protected function getInsertSuffix( $table ) + { + return ''; + } + + /** + * Checks whether a value starts with zeros. In this case + * the value should probably be stored using a text datatype instead of a + * numerical type in order to preserve the zeros. + * + * @param string $value value to be checked. + * + * @return boolean + */ + protected function startsWithZeros( $value ) + { + $value = strval( $value ); + + if ( strlen( $value ) > 1 && strpos( $value, '0' ) === 0 && strpos( $value, '0.' ) !== 0 ) { + return TRUE; + } else { + return FALSE; + } + } + + /** + * Inserts a record into the database using a series of insert columns + * and corresponding insertvalues. Returns the insert id. + * + * @param string $table table to perform query on + * @param array $insertcolumns columns to be inserted + * @param array $insertvalues values to be inserted + * + * @return integer + */ + protected function insertRecord( $type, $insertcolumns, $insertvalues ) + { + $default = $this->defaultValue; + $suffix = $this->getInsertSuffix( $type ); + $table = $this->esc( $type ); + + if ( count( $insertvalues ) > 0 && is_array( $insertvalues[0] ) && count( $insertvalues[0] ) > 0 ) { + + $insertSlots = array(); + foreach ( $insertcolumns as $k => $v ) { + $insertcolumns[$k] = $this->esc( $v ); + + if (isset(self::$sqlFilters['w'][$type][$v])) { + $insertSlots[] = self::$sqlFilters['w'][$type][$v]; + } else { + $insertSlots[] = '?'; + } + } + + $insertSQL = "INSERT INTO $table ( id, " . implode( ',', $insertcolumns ) . " ) VALUES + ( $default, " . implode( ',', $insertSlots ) . " ) $suffix"; + + $ids = array(); + foreach ( $insertvalues as $i => $insertvalue ) { + $ids[] = $this->adapter->getCell( $insertSQL, $insertvalue, $i ); + } + + $result = count( $ids ) === 1 ? array_pop( $ids ) : $ids; + } else { + $result = $this->adapter->getCell( "INSERT INTO $table (id) VALUES($default) $suffix" ); + } + + if ( $suffix ) return $result; + + $last_id = $this->adapter->getInsertID(); + + return $last_id; + } + + /** + * Checks table name or column name. + * + * @param string $table table string + * + * @return string + */ + protected function check( $struct ) + { + if ( !is_string( $struct ) || !preg_match( '/^[a-zA-Z0-9_]+$/', $struct ) ) { + throw new RedException( 'Identifier does not conform to RedBeanPHP security policies.' ); + } + + return $struct; + } + + /** + * Checks whether the specified type (i.e. table) already exists in the database. + * Not part of the Object Database interface! + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + $tables = $this->getTables(); + + return in_array( $table, $tables ); + } + + /** + * @see QueryWriter::glueSQLCondition + */ + public function glueSQLCondition( $sql, $glue = NULL ) + { + static $snippetCache = array(); + + if ( trim( $sql ) === '' ) { + return $sql; + } + + $key = $glue . '|' . $sql; + + if ( isset( $snippetCache[$key] ) ) { + return $snippetCache[$key]; + } + + $lsql = ltrim( $sql ); + + if ( preg_match( '/^(INNER|LEFT|RIGHT|JOIN|AND|OR|WHERE|ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', $lsql ) ) { + if ( $glue === QueryWriter::C_GLUE_WHERE && stripos( $lsql, 'AND' ) === 0 ) { + $snippetCache[$key] = ' WHERE ' . substr( $lsql, 3 ); + } else { + $snippetCache[$key] = $sql; + } + } else { + $snippetCache[$key] = ( ( $glue === QueryWriter::C_GLUE_AND ) ? ' AND ' : ' WHERE ') . $sql; + } + + return $snippetCache[$key]; + } + + /** + * @see QueryWriter::glueLimitOne + */ + public function glueLimitOne( $sql = '') + { + return ( strpos( strtoupper( ' ' . $sql ), ' LIMIT ' ) === FALSE ) ? ( $sql . ' LIMIT 1 ' ) : $sql; + } + + /** + * @see QueryWriter::esc + */ + public function esc( $dbStructure, $dontQuote = FALSE ) + { + $this->check( $dbStructure ); + + return ( $dontQuote ) ? $dbStructure : $this->quoteCharacter . $dbStructure . $this->quoteCharacter; + } + + /** + * @see QueryWriter::addColumn + */ + public function addColumn( $beanType, $column, $field ) + { + $table = $beanType; + $type = $field; + $table = $this->esc( $table ); + $column = $this->esc( $column ); + + $type = ( isset( $this->typeno_sqltype[$type] ) ) ? $this->typeno_sqltype[$type] : ''; + + $this->adapter->exec( sprintf( $this->getDDLTemplate('addColumn', $beanType, $column ), $table, $column, $type ) ); + } + + /** + * @see QueryWriter::updateRecord + */ + public function updateRecord( $type, $updatevalues, $id = NULL ) + { + $table = $type; + + if ( !$id ) { + $insertcolumns = $insertvalues = array(); + + foreach ( $updatevalues as $pair ) { + $insertcolumns[] = $pair['property']; + $insertvalues[] = $pair['value']; + } + + //Otherwise psql returns string while MySQL/SQLite return numeric causing problems with additions (array_diff) + return (string) $this->insertRecord( $table, $insertcolumns, array( $insertvalues ) ); + } + + if ( $id && !count( $updatevalues ) ) { + return $id; + } + + $table = $this->esc( $table ); + $sql = "UPDATE $table SET "; + + $p = $v = array(); + + foreach ( $updatevalues as $uv ) { + + if ( isset( self::$sqlFilters['w'][$type][$uv['property']] ) ) { + $p[] = " {$this->esc( $uv["property"] )} = ". self::$sqlFilters['w'][$type][$uv['property']]; + } else { + $p[] = " {$this->esc( $uv["property"] )} = ? "; + } + + $v[] = $uv['value']; + } + + $sql .= implode( ',', $p ) . ' WHERE id = ? '; + + $v[] = $id; + + $this->adapter->exec( $sql, $v ); + + return $id; + } + + /** + * @see QueryWriter::parseJoin + */ + public function parseJoin( $type, $sql, $cteType = NULL ) + { + if ( strpos( $sql, '@' ) === FALSE ) { + return $sql; + } + + $sql = ' ' . $sql; + $joins = array(); + $joinSql = ''; + + if ( !preg_match_all( '#@((shared|own|joined)\.[^\s(,=!?]+)#', $sql, $matches ) ) + return $sql; + + $expressions = $matches[1]; + // Sort to make the joins from the longest to the shortest + uasort( $expressions, function($a, $b) { + return substr_count( $b, '.' ) - substr_count( $a, '.' ); + }); + + $nsuffix = 1; + foreach ( $expressions as $exp ) { + $explosion = explode( '.', $exp ); + $joinTable = $type; + $joinType = array_shift( $explosion ); + $lastPart = array_pop( $explosion ); + $lastJoin = end($explosion); + if ( ( $index = strpos( $lastJoin, '[' ) ) !== FALSE ) { + $lastJoin = substr( $lastJoin, 0, $index); + } + reset($explosion); + + // Let's check if we already joined that chain + // If that's the case we skip this + $joinKey = implode( '.', $explosion ); + foreach ( $joins as $chain => $suffix ) { + if ( strpos ( $chain, $joinKey ) === 0 ) { + $sql = str_replace( "@{$exp}", "{$lastJoin}__rb{$suffix}.{$lastPart}", $sql ); + continue 2; + } + } + $sql = str_replace( "@{$exp}", "{$lastJoin}__rb{$nsuffix}.{$lastPart}", $sql ); + $joins[$joinKey] = $nsuffix; + + // We loop on the elements of the join + $i = 0; + while ( TRUE ) { + $joinInfo = $explosion[$i]; + if ( $i ) { + $joinType = $explosion[$i-1]; + $joinTable = $explosion[$i-2]; + } + + $aliases = array(); + if ( ( $index = strpos( $joinInfo, '[' ) ) !== FALSE ) { + if ( preg_match_all( '#(([^\s:/\][]+)[/\]])#', $joinInfo, $matches ) ) { + $aliases = $matches[2]; + $joinInfo = substr( $joinInfo, 0, $index); + } + } + if ( ( $index = strpos( $joinTable, '[' ) ) !== FALSE ) { + $joinTable = substr( $joinTable, 0, $index); + } + + if ( $i ) { + $joinSql .= $this->writeJoin( $joinTable, $joinInfo, 'INNER', $joinType, FALSE, "__rb{$nsuffix}", $aliases, NULL ); + } else { + $joinSql .= $this->writeJoin( $joinTable, $joinInfo, 'LEFT', $joinType, TRUE, "__rb{$nsuffix}", $aliases, $cteType ); + } + + $i += 2; + if ( !isset( $explosion[$i] ) ) { + break; + } + } + $nsuffix++; + } + + $sql = str_ireplace( ' where ', ' WHERE ', $sql ); + if ( strpos( $sql, ' WHERE ') === FALSE ) { + if ( preg_match( '/^(ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', trim($sql) ) ) { + $sql = "{$joinSql} {$sql}"; + } else { + $sql = "{$joinSql} WHERE {$sql}"; + } + } else { + $sqlParts = explode( ' WHERE ', $sql, 2 ); + $sql = "{$sqlParts[0]} {$joinSql} WHERE {$sqlParts[1]}"; + } + + return $sql; + } + + /** + * @see QueryWriter::writeJoin + */ + public function writeJoin( $type, $targetType, $leftRight = 'LEFT', $joinType = 'parent', $firstOfChain = TRUE, $suffix = '', $aliases = array(), $cteType = NULL ) + { + if ( $leftRight !== 'LEFT' && $leftRight !== 'RIGHT' && $leftRight !== 'INNER' ) + throw new RedException( 'Invalid JOIN.' ); + + $globalAliases = OODBBean::getAliases(); + if ( isset( $globalAliases[$targetType] ) ) { + $destType = $globalAliases[$targetType]; + $asTargetTable = $this->esc( $targetType.$suffix ); + } else { + $destType = $targetType; + $asTargetTable = $this->esc( $destType.$suffix ); + } + + if ( $firstOfChain ) { + $table = $this->esc( $type ); + } else { + $table = $this->esc( $type.$suffix ); + } + $targetTable = $this->esc( $destType ); + + if ( $joinType == 'shared' ) { + + if ( isset( $globalAliases[$type] ) ) { + $field = $this->esc( $globalAliases[$type], TRUE ); + if ( $aliases && count( $aliases ) === 1 ) { + $assocTable = reset( $aliases ); + } else { + $assocTable = $this->getAssocTable( array( $cteType ? $cteType : $globalAliases[$type], $destType ) ); + } + } else { + $field = $this->esc( $type, TRUE ); + if ( $aliases && count( $aliases ) === 1 ) { + $assocTable = reset( $aliases ); + } else { + $assocTable = $this->getAssocTable( array( $cteType ? $cteType : $type, $destType ) ); + } + } + $linkTable = $this->esc( $assocTable ); + $asLinkTable = $this->esc( $assocTable.$suffix ); + $leftField = "id"; + $rightField = $cteType ? "{$cteType}_id" : "{$field}_id"; + $linkField = $this->esc( $destType, TRUE ); + $linkLeftField = "id"; + $linkRightField = "{$linkField}_id"; + + $joinSql = " {$leftRight} JOIN {$linkTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asLinkTable}"; + } + $joinSql .= " ON {$table}.{$leftField} = {$asLinkTable}.{$rightField}"; + $joinSql .= " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + $joinSql .= " ON {$asTargetTable}.{$linkLeftField} = {$asLinkTable}.{$linkRightField}"; + + } elseif ( $joinType == 'own' ) { + + $field = $this->esc( $type, TRUE ); + $rightField = "id"; + + $joinSql = " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + + if ( $aliases ) { + $conditions = array(); + foreach ( $aliases as $alias ) { + $conditions[] = "{$asTargetTable}.{$alias}_id = {$table}.{$rightField}"; + } + $joinSql .= " ON ( " . implode( ' OR ', $conditions ) . " ) "; + } else { + $leftField = $cteType ? "{$cteType}_id" : "{$field}_id"; + $joinSql .= " ON {$asTargetTable}.{$leftField} = {$table}.{$rightField} "; + } + + } else { + + $field = $this->esc( $targetType, TRUE ); + $leftField = "id"; + + $joinSql = " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + + if ( $aliases ) { + $conditions = array(); + foreach ( $aliases as $alias ) { + $conditions[] = "{$asTargetTable}.{$leftField} = {$table}.{$alias}_id"; + } + $joinSql .= " ON ( " . implode( ' OR ', $conditions ) . " ) "; + } else { + $rightField = "{$field}_id"; + $joinSql .= " ON {$asTargetTable}.{$leftField} = {$table}.{$rightField} "; + } + + } + + return $joinSql; + } + + /** + * Sets an SQL snippet to be used for the next queryRecord() operation. + * A select snippet will be inserted at the end of the SQL select statement and + * can be used to modify SQL-select commands to enable locking, for instance + * using the 'FOR UPDATE' snippet (this will generate an SQL query like: + * 'SELECT * FROM ... FOR UPDATE'. After the query has been executed the + * SQL snippet will be erased. Note that only the first upcoming direct or + * indirect invocation of queryRecord() through batch(), find() or load() + * will be affected. The SQL snippet will be cached. + * + * @param string $sql SQL snippet to use in SELECT statement. + * + * return self + */ + public function setSQLSelectSnippet( $sqlSelectSnippet = '' ) { + $this->sqlSelectSnippet = $sqlSelectSnippet; + return $this; + } + + /** + * @see QueryWriter::queryRecord + */ + public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + if ( $this->flagUseCache && $this->sqlSelectSnippet != self::C_SELECT_SNIPPET_FOR_UPDATE ) { + $key = $this->getCacheKey( array( $conditions, trim("$addSql {$this->sqlSelectSnippet}"), $bindings, 'select' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $table = $this->esc( $type ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $type ); + } + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + $sql = $this->parseJoin( $type, $sql ); + $fieldSelection = self::$flagNarrowFieldMode ? "{$table}.*" : '*'; + $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} {$this->sqlSelectSnippet} -- keep-cache"; + $this->sqlSelectSnippet = ''; + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache && !empty( $key ) ) { + $this->putResultInCache( $type, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryRecordWithCursor + */ + public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() ) + { + $table = $this->esc( $type ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $type ); + } + + $sql = $this->glueSQLCondition( $addSql, NULL ); + + $sql = $this->parseJoin( $type, $sql ); + $fieldSelection = self::$flagNarrowFieldMode ? "{$table}.*" : '*'; + + $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} -- keep-cache"; + + return $this->adapter->getCursor( $sql, $bindings ); + } + + /** + * @see QueryWriter::queryRecordRelated + */ + public function queryRecordRelated( $sourceType, $destType, $linkIDs, $addSql = '', $bindings = array() ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $sourceType, implode( ',', $linkIDs ), trim($addSql), $bindings, 'selectrelated' ) ); + if ( $cached = $this->getCached( $destType, $key ) ) { + return $cached; + } + } + + $addSql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE ); + $inClause = $this->getParametersForInClause( $linkIDs, $bindings ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $destType ); + } + + if ( $sourceType === $destType ) { + $inClause2 = $this->getParametersForInClause( $linkIDs, $bindings, count( $bindings ) ); //for some databases + $sql = " + SELECT + {$destTable}.* {$sqlFilterStr} , + COALESCE( + NULLIF({$linkTable}.{$sourceCol}, {$destTable}.id), + NULLIF({$linkTable}.{$destCol}, {$destTable}.id)) AS linked_by + FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) OR + ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} IN ($inClause2) ) + {$addSql} + -- keep-cache"; + + $linkIDs = array_merge( $linkIDs, $linkIDs ); + } else { + $sql = " + SELECT + {$destTable}.* {$sqlFilterStr}, + {$linkTable}.{$sourceCol} AS linked_by + FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) + {$addSql} + -- keep-cache"; + } + + $bindings = array_merge( $linkIDs, $bindings ); + + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $destType, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryRecordLink + */ + public function queryRecordLink( $sourceType, $destType, $sourceID, $destID ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $sourceType, $destType, $sourceID, $destID, 'selectlink' ) ); + if ( $cached = $this->getCached( $linkTable, $key ) ) { + return $cached; + } + } + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $linkType = $this->getAssocTable( array( $sourceType, $destType ) ); + $sqlFilterStr = $this->getSQLFilterSnippet( "{$linkType}" ); + } + + if ( $sourceTable === $destTable ) { + $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable} + WHERE ( {$sourceCol} = ? AND {$destCol} = ? ) OR + ( {$destCol} = ? AND {$sourceCol} = ? ) -- keep-cache"; + $row = $this->adapter->getRow( $sql, array( $sourceID, $destID, $sourceID, $destID ) ); + } else { + $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable} + WHERE {$sourceCol} = ? AND {$destCol} = ? -- keep-cache"; + $row = $this->adapter->getRow( $sql, array( $sourceID, $destID ) ); + } + + if ( $this->flagUseCache ) { + $this->putResultInCache( $linkTable, $key, $row ); + } + + return $row; + } + + /** + * Returns or counts all rows of specified type that have been tagged with one of the + * strings in the specified tag list array. + * + * Note that the additional SQL snippet can only be used for pagination, + * the SQL snippet will be appended to the end of the query. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * @param string $wrap SQL wrapper string (use %s for subquery) + * + * @return array + */ + private function queryTaggedGeneric( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array(), $wrap = '%s' ) + { + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( implode( ',', $tagList ), $all, trim($addSql), $bindings, 'selectTagged' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $assocType = $this->getAssocTable( array( $type, 'tag' ) ); + $assocTable = $this->esc( $assocType ); + $assocField = $type . '_id'; + $table = $this->esc( $type ); + $slots = implode( ',', array_fill( 0, count( $tagList ), '?' ) ); + $score = ( $all ) ? count( $tagList ) : 1; + + $sql = " + SELECT {$table}.* FROM {$table} + INNER JOIN {$assocTable} ON {$assocField} = {$table}.id + INNER JOIN tag ON {$assocTable}.tag_id = tag.id + WHERE tag.title IN ({$slots}) + GROUP BY {$table}.id + HAVING count({$table}.id) >= ? + {$addSql} + -- keep-cache + "; + $sql = sprintf($wrap,$sql); + + $bindings = array_merge( $tagList, array( $score ), $bindings ); + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $type, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryTagged + */ + public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ) + { + return $this->queryTaggedGeneric( $type, $tagList, $all, $addSql, $bindings ); + } + + /** + * @see QueryWriter::queryCountTagged + */ + public function queryCountTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ) + { + $rows = $this->queryTaggedGeneric( $type, $tagList, $all, $addSql, $bindings, 'SELECT COUNT(*) AS counted FROM (%s) AS counting' ); + return intval($rows[0]['counted']); + } + + /** + * @see QueryWriter::queryRecordCount + */ + public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $conditions, trim($addSql), $bindings, 'count' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $table = $this->esc( $type ); + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + + $sql = $this->parseJoin( $type, $sql ); + + $sql = "SELECT COUNT(*) FROM {$table} {$sql} -- keep-cache"; + $count = (int) $this->adapter->getCell( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $type, $key, $count ); + } + + return $count; + } + + /** + * @see QueryWriter::queryRecordCountRelated + */ + public function queryRecordCountRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $cacheType = "#{$sourceType}/{$destType}"; + $key = $this->getCacheKey( array( $sourceType, $destType, $linkID, trim($addSql), $bindings, 'countrelated' ) ); + if ( $cached = $this->getCached( $cacheType, $key ) ) { + return $cached; + } + } + + if ( $sourceType === $destType ) { + $sql = " + SELECT COUNT(*) FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) OR + ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} = ? ) + {$addSql} + -- keep-cache"; + + $bindings = array_merge( array( $linkID, $linkID ), $bindings ); + } else { + $sql = " + SELECT COUNT(*) FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) + {$addSql} + -- keep-cache"; + + $bindings = array_merge( array( $linkID ), $bindings ); + } + + $count = (int) $this->adapter->getCell( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $cacheType, $key, $count ); + } + + return $count; + } + + /** + * @see QueryWriter::queryRecursiveCommonTableExpression + */ + public function queryRecursiveCommonTableExpression( $type, $id, $up = TRUE, $addSql = NULL, $bindings = array(), $selectForm = FALSE ) + { + if ($selectForm === QueryWriter::C_CTE_SELECT_COUNT) { + $selectForm = "count(redbeantree.*)"; + } elseif ( $selectForm === QueryWriter::C_CTE_SELECT_NORMAL ) { + $selectForm = "redbeantree.*"; + } + $alias = $up ? 'parent' : 'child'; + $direction = $up ? " {$alias}.{$type}_id = {$type}.id " : " {$alias}.id = {$type}.{$type}_id "; + /* allow numeric and named param bindings, if '0' exists then numeric */ + if ( array_key_exists( 0,$bindings ) ) { + array_unshift( $bindings, $id ); + $idSlot = '?'; + } else { + $idSlot = ':slot0'; + $bindings[$idSlot] = $id; + } + $sql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE ); + $sql = $this->parseJoin( 'redbeantree', $sql, $type ); + $rows = $this->adapter->get(" + WITH RECURSIVE redbeantree AS + ( + SELECT * + FROM {$type} WHERE {$type}.id = {$idSlot} + UNION ALL + SELECT {$type}.* FROM {$type} + INNER JOIN redbeantree {$alias} ON {$direction} + ) + SELECT {$selectForm} FROM redbeantree {$sql};", + $bindings + ); + return $rows; + } + + /** + * @see QueryWriter::deleteRecord + */ + public function deleteRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + $table = $this->esc( $type ); + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + + $sql = "DELETE FROM {$table} {$sql}"; + + return $this->adapter->exec( $sql, $bindings ); + } + + /** + * @see QueryWriter::deleteRelations + */ + public function deleteRelations( $sourceType, $destType, $sourceID ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $sourceTable === $destTable ) { + $sql = "DELETE FROM {$linkTable} + WHERE ( {$sourceCol} = ? ) OR + ( {$destCol} = ? ) + "; + + $this->adapter->exec( $sql, array( $sourceID, $sourceID ) ); + } else { + $sql = "DELETE FROM {$linkTable} + WHERE {$sourceCol} = ? "; + + $this->adapter->exec( $sql, array( $sourceID ) ); + } + } + + /** + * @see QueryWriter::widenColumn + */ + public function widenColumn( $type, $property, $dataType ) + { + if ( !isset($this->typeno_sqltype[$dataType]) ) return FALSE; + + $table = $this->esc( $type ); + $column = $this->esc( $property ); + + $newType = $this->typeno_sqltype[$dataType]; + + $this->adapter->exec( sprintf( $this->getDDLTemplate( 'widenColumn', $type, $column ), $type, $column, $column, $newType ) ); + + return TRUE; + } + + /** + * @see QueryWriter::wipe + */ + public function wipe( $type ) + { + $table = $this->esc( $type ); + + $this->adapter->exec( "TRUNCATE $table " ); + } + + /** + * @see QueryWriter::renameAssocTable + */ + public function renameAssocTable( $from, $to = NULL ) + { + self::renameAssociation( $from, $to ); + } + + /** + * @see QueryWriter::getAssocTable + */ + public function getAssocTable( $types ) + { + return self::getAssocTableFormat( $types ); + } + + /** + * Turns caching on or off. Default: off. + * If caching is turned on retrieval queries fired after eachother will + * use a result row cache. + * + * @param boolean + * + * @return void + */ + public function setUseCache( $yesNo ) + { + $this->flushCache(); + + $this->flagUseCache = (bool) $yesNo; + } + + /** + * Flushes the Query Writer Cache. + * Clears the internal query cache array and returns its overall + * size. + * + * @return mixed + */ + public function flushCache( $newMaxCacheSizePerType = NULL, $countCache = TRUE ) + { + if ( !is_null( $newMaxCacheSizePerType ) && $newMaxCacheSizePerType > 0 ) { + $this->maxCacheSizePerType = $newMaxCacheSizePerType; + } + $count = $countCache ? count( $this->cache, COUNT_RECURSIVE ) : NULL; + $this->cache = array(); + return $count; + } + + /** + * @deprecated Use esc() instead. + * + * @param string $column column to be escaped + * @param boolean $noQuotes omit quotes + * + * @return string + */ + public function safeColumn( $column, $noQuotes = FALSE ) + { + return $this->esc( $column, $noQuotes ); + } + + /** + * @deprecated Use esc() instead. + * + * @param string $table table to be escaped + * @param boolean $noQuotes omit quotes + * + * @return string + */ + public function safeTable( $table, $noQuotes = FALSE ) + { + return $this->esc( $table, $noQuotes ); + } + + /** + * @see QueryWriter::addUniqueConstraint + */ + public function addUniqueIndex( $type, $properties ) + { + return $this->addUniqueConstraint( $type, $properties ); + } +} +} + +namespace RedBeanPHP\QueryWriter { + +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\Adapter as Adapter; +use RedBeanPHP\RedException\SQL as SQLException; + +/** + * RedBeanPHP SQLiteWriter with support for SQLite types + * This is a QueryWriter class for RedBeanPHP. + * This QueryWriter provides support for the SQLite database platform. + * + * @file RedBeanPHP/QueryWriter/SQLiteT.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SQLiteT extends AQueryWriter implements QueryWriter +{ + /** + * Data types + */ + const C_DATATYPE_INTEGER = 0; + const C_DATATYPE_NUMERIC = 1; + const C_DATATYPE_TEXT = 2; + const C_DATATYPE_SPECIFIED = 99; + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * @var string + */ + protected $quoteCharacter = '`'; + + /** + * @var array + */ + protected $DDLTemplates = array( + 'addColumn' => array( + '*' => 'ALTER TABLE `%s` ADD `%s` %s' + ), + 'createTable' => array( + '*' => 'CREATE TABLE %s ( id INTEGER PRIMARY KEY AUTOINCREMENT )' + ), + 'widenColumn' => array( + '*' => ',`%s` %s ' + ) + ); + + /** + * Gets all information about a table (from a type). + * + * Format: + * array( + * name => name of the table + * columns => array( name => datatype ) + * indexes => array() raw index information rows from PRAGMA query + * keys => array() raw key information rows from PRAGMA query + * ) + * + * @param string $type type you want to get info of + * + * @return array + */ + protected function getTable( $type ) + { + $tableName = $this->esc( $type, TRUE ); + $columns = $this->getColumns( $type ); + $indexes = $this->getIndexes( $type ); + $keys = $this->getKeyMapForType( $type ); + + $table = array( + 'columns' => $columns, + 'indexes' => $indexes, + 'keys' => $keys, + 'name' => $tableName + ); + + $this->tableArchive[$tableName] = $table; + + return $table; + } + + /** + * Puts a table. Updates the table structure. + * In SQLite we can't change columns, drop columns, change or add foreign keys so we + * have a table-rebuild function. You simply load your table with getTable(), modify it and + * then store it with putTable()... + * + * @param array $tableMap information array + * + * @return void + */ + protected function putTable( $tableMap ) + { + $table = $tableMap['name']; + $q = array(); + $q[] = "DROP TABLE IF EXISTS tmp_backup;"; + + $oldColumnNames = array_keys( $this->getColumns( $table ) ); + + foreach ( $oldColumnNames as $k => $v ) $oldColumnNames[$k] = "`$v`"; + + $q[] = "CREATE TEMPORARY TABLE tmp_backup(" . implode( ",", $oldColumnNames ) . ");"; + $q[] = "INSERT INTO tmp_backup SELECT * FROM `$table`;"; + $q[] = "PRAGMA foreign_keys = 0 "; + $q[] = "DROP TABLE `$table`;"; + + $newTableDefStr = ''; + foreach ( $tableMap['columns'] as $column => $type ) { + if ( $column != 'id' ) { + $newTableDefStr .= sprintf( $this->getDDLTemplate( 'widenColumn', $table, $column ), $column, $type ); + } + } + + $fkDef = ''; + foreach ( $tableMap['keys'] as $key ) { + $fkDef .= ", FOREIGN KEY(`{$key['from']}`) + REFERENCES `{$key['table']}`(`{$key['to']}`) + ON DELETE {$key['on_delete']} ON UPDATE {$key['on_update']}"; + } + + $q[] = "CREATE TABLE `$table` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT $newTableDefStr $fkDef );"; + + foreach ( $tableMap['indexes'] as $name => $index ) { + if ( strpos( $name, 'UQ_' ) === 0 ) { + $cols = explode( '__', substr( $name, strlen( 'UQ_' . $table ) ) ); + foreach ( $cols as $k => $v ) $cols[$k] = "`$v`"; + $q[] = "CREATE UNIQUE INDEX $name ON `$table` (" . implode( ',', $cols ) . ")"; + } else $q[] = "CREATE INDEX $name ON `$table` ({$index['name']}) "; + } + + $q[] = "INSERT INTO `$table` SELECT * FROM tmp_backup;"; + $q[] = "DROP TABLE tmp_backup;"; + $q[] = "PRAGMA foreign_keys = 1 "; + + foreach ( $q as $sq ) $this->adapter->exec( $sq ); + } + + /** + * Returns the an array describing the indexes for type $type. + * + * @param string $type type to describe indexes of + * + * @return array + */ + protected function getIndexes( $type ) + { + $table = $this->esc( $type, TRUE ); + $indexes = $this->adapter->get( "PRAGMA index_list('$table')" ); + + $indexInfoList = array(); + foreach ( $indexes as $i ) { + $indexInfoList[$i['name']] = $this->adapter->getRow( "PRAGMA index_info('{$i['name']}') " ); + + $indexInfoList[$i['name']]['unique'] = $i['unique']; + } + + return $indexInfoList; + } + + /** + * Adds a foreign key to a type. + * Note: cant put this in try-catch because that can hide the fact + * that database has been damaged. + * + * @param string $type type you want to modify table of + * @param string $targetType target type + * @param string $field field of the type that needs to get the fk + * @param string $targetField field where the fk needs to point to + * @param integer $buildopt 0 = NO ACTION, 1 = ON DELETE CASCADE + * + * @return boolean + */ + protected function buildFK( $type, $targetType, $property, $targetProperty, $constraint = FALSE ) + { + $table = $this->esc( $type, TRUE ); + $targetTable = $this->esc( $targetType, TRUE ); + $column = $this->esc( $property, TRUE ); + $targetColumn = $this->esc( $targetProperty, TRUE ); + + $tables = $this->getTables(); + if ( !in_array( $targetTable, $tables ) ) return FALSE; + + if ( !is_null( $this->getForeignKeyForTypeProperty( $table, $column ) ) ) return FALSE; + $t = $this->getTable( $table ); + $consSQL = ( $constraint ? 'CASCADE' : 'SET NULL' ); + $label = 'from_' . $column . '_to_table_' . $targetTable . '_col_' . $targetColumn; + $t['keys'][$label] = array( + 'table' => $targetTable, + 'from' => $column, + 'to' => $targetColumn, + 'on_update' => $consSQL, + 'on_delete' => $consSQL + ); + $this->putTable( $t ); + return TRUE; + } + + /** + * @see AQueryWriter::getKeyMapForType + */ + protected function getKeyMapForType( $type ) + { + $table = $this->esc( $type, TRUE ); + $keys = $this->adapter->get( "PRAGMA foreign_key_list('$table')" ); + $keyInfoList = array(); + foreach ( $keys as $k ) { + $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); + $keyInfoList[$label] = array( + 'name' => $label, + 'from' => $k['from'], + 'table' => $k['table'], + 'to' => $k['to'], + 'on_update' => $k['on_update'], + 'on_delete' => $k['on_delete'] + ); + } + return $keyInfoList; + } + + /** + * Constructor + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Adapter $adapter Database Adapter + */ + public function __construct( Adapter $adapter ) + { + $this->typeno_sqltype = array( + SQLiteT::C_DATATYPE_INTEGER => 'INTEGER', + SQLiteT::C_DATATYPE_NUMERIC => 'NUMERIC', + SQLiteT::C_DATATYPE_TEXT => 'TEXT', + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[$v] = $k; + } + + $this->adapter = $adapter; + $this->adapter->setOption( 'setInitQuery', ' PRAGMA foreign_keys = 1 ' ); + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer $const data type to be used for IDS. + */ + public function getTypeForID() + { + return self::C_DATATYPE_INTEGER; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + $this->svalue = $value; + + if ( $value === NULL ) return self::C_DATATYPE_INTEGER; + if ( $value === INF ) return self::C_DATATYPE_TEXT; + + if ( $this->startsWithZeros( $value ) ) return self::C_DATATYPE_TEXT; + + if ( $value === TRUE || $value === FALSE ) return self::C_DATATYPE_INTEGER; + + if ( is_numeric( $value ) && ( intval( $value ) == $value ) && $value < 2147483648 && $value > -2147483648 ) return self::C_DATATYPE_INTEGER; + + if ( ( is_numeric( $value ) && $value < 2147483648 && $value > -2147483648) + || preg_match( '/\d{4}\-\d\d\-\d\d/', $value ) + || preg_match( '/\d{4}\-\d\d\-\d\d\s\d\d:\d\d:\d\d/', $value ) + ) { + return self::C_DATATYPE_NUMERIC; + } + + return self::C_DATATYPE_TEXT; + } + + /** + * @see QueryWriter::addColumn + */ + public function addColumn( $table, $column, $type ) + { + $column = $this->check( $column ); + $table = $this->check( $table ); + $type = $this->typeno_sqltype[$type]; + + $this->adapter->exec( sprintf( $this->getDDLTemplate( 'addColumn', $table, $column ), $table, $column, $type ) ); + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + $r = ( ( isset( $this->sqltype_typeno[$typedescription] ) ) ? $this->sqltype_typeno[$typedescription] : 99 ); + + return $r; + } + + /** + * @see QueryWriter::widenColumn + */ + public function widenColumn( $type, $column, $datatype ) + { + $t = $this->getTable( $type ); + + $t['columns'][$column] = $this->typeno_sqltype[$datatype]; + + $this->putTable( $t ); + } + + /** + * @see QueryWriter::getTables(); + */ + public function getTables() + { + return $this->adapter->getCol( "SELECT name FROM sqlite_master + WHERE type='table' AND name!='sqlite_sequence';" ); + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $type ) + { + $table = $this->esc( $type ); + + $sql = sprintf( $this->getDDLTemplate( 'createTable', $type ), $table ); + + $this->adapter->exec( $sql ); + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $table = $this->esc( $table, TRUE ); + + $columnsRaw = $this->adapter->get( "PRAGMA table_info('$table')" ); + + $columns = array(); + foreach ( $columnsRaw as $r ) $columns[$r['name']] = $r['type']; + + return $columns; + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $name = 'UQ_' . $this->esc( $type, TRUE ) . implode( '__', $properties ); + $t = $this->getTable( $type ); + $t['indexes'][$name] = array( 'name' => $name ); + try { + $this->putTable( $t ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + $stateMap = array( + '23000' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION + ); + if ( $state == 'HY000' + && isset($extraDriverDetails[1]) + && $extraDriverDetails[1] == 1 + && ( in_array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, $list ) + || in_array( QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, $list ) + )) { + return TRUE; + } + return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); + } + + /** + * Sets an SQL snippet to be used for the next queryRecord() operation. + * SQLite has no SELECT-FOR-UPDATE and filters this. + * + * @param string $sql SQL snippet to use in SELECT statement. + * + * return self + */ + public function setSQLSelectSnippet( $sqlSelectSnippet = '' ) { + if ( $sqlSelectSnippet === AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE) $sqlSelectSnippet = ''; + $this->sqlSelectSnippet = $sqlSelectSnippet; + return $this; + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $column ) + { + $columns = $this->getColumns( $type ); + if ( !isset( $columns[$column] ) ) return FALSE; + + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $column, TRUE ); + + try { + $t = $this->getTable( $type ); + $t['indexes'][$name] = array( 'name' => $column ); + $this->putTable( $t ); + return TRUE; + } catch( SQLException $exception ) { + return FALSE; + } + } + + /** + * @see QueryWriter::wipe + */ + public function wipe( $type ) + { + $table = $this->esc( $type ); + + $this->adapter->exec( "DELETE FROM $table " ); + } + + /** + * @see QueryWriter::addFK + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ) + { + return $this->buildFK( $type, $targetType, $property, $targetProperty, $isDep ); + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + $this->adapter->exec( 'PRAGMA foreign_keys = 0 ' ); + + foreach ( $this->getTables() as $t ) { + try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; } + try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; } + } + + $this->adapter->exec( 'PRAGMA foreign_keys = 1 ' ); + } +} +} + +namespace RedBeanPHP\QueryWriter { +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\Adapter as Adapter; +use RedBeanPHP\RedException\SQL as SQLException; + +/** + * RedBeanPHP CUBRID Writer. + * This is a QueryWriter class for RedBeanPHP. + * This QueryWriter provides support for the CUBRID database platform. + * + * @file RedBeanPHP/QueryWriter/CUBRID.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class CUBRID extends AQueryWriter implements QueryWriter +{ + /** + * Data types + */ + const C_DATATYPE_INTEGER = 0; + const C_DATATYPE_DOUBLE = 1; + const C_DATATYPE_STRING = 2; + const C_DATATYPE_SPECIAL_DATE = 80; + const C_DATATYPE_SPECIAL_DATETIME = 81; + const C_DATATYPE_SPECIFIED = 99; + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * @var string + */ + protected $quoteCharacter = '`'; + + /** + * This method adds a foreign key from type and field to + * target type and target field. + * The foreign key is created without an action. On delete/update + * no action will be triggered. The FK is only used to allow database + * tools to generate pretty diagrams and to make it easy to add actions + * later on. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type that will have a foreign key field + * @param string $targetType points to this type + * @param string $property field that contains the foreign key value + * @param string $targetProperty field where the fk points to + * @param boolean $isDep is dependent + * + * @return bool + */ + protected function buildFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ) + { + $table = $this->esc( $type ); + $tableNoQ = $this->esc( $type, TRUE ); + $targetTable = $this->esc( $targetType ); + $targetTableNoQ = $this->esc( $targetType, TRUE ); + $column = $this->esc( $property ); + $columnNoQ = $this->esc( $property, TRUE ); + $targetColumn = $this->esc( $targetProperty ); + if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $columnNoQ ) ) ) return FALSE; + $needsToDropFK = FALSE; + $casc = ( $isDep ? 'CASCADE' : 'SET NULL' ); + $sql = "ALTER TABLE $table ADD CONSTRAINT FOREIGN KEY($column) REFERENCES $targetTable($targetColumn) ON DELETE $casc "; + try { + $this->adapter->exec( $sql ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see AQueryWriter::getKeyMapForType + */ + protected function getKeyMapForType( $type ) + { + $sqlCode = $this->adapter->get("SHOW CREATE TABLE `{$type}`"); + if (!isset($sqlCode[0])) return array(); + $matches = array(); + preg_match_all( '/CONSTRAINT\s+\[([\w_]+)\]\s+FOREIGN\s+KEY\s+\(\[([\w_]+)\]\)\s+REFERENCES\s+\[([\w_]+)\](\s+ON\s+DELETE\s+(CASCADE|SET\sNULL|RESTRICT|NO\sACTION)\s+ON\s+UPDATE\s+(SET\sNULL|RESTRICT|NO\sACTION))?/', $sqlCode[0]['CREATE TABLE'], $matches ); + $list = array(); + if (!isset($matches[0])) return $list; + $max = count($matches[0]); + for($i = 0; $i < $max; $i++) { + $label = $this->makeFKLabel( $matches[2][$i], $matches[3][$i], 'id' ); + $list[ $label ] = array( + 'name' => $matches[1][$i], + 'from' => $matches[2][$i], + 'table' => $matches[3][$i], + 'to' => 'id', + 'on_update' => $matches[6][$i], + 'on_delete' => $matches[5][$i] + ); + } + return $list; + } + + /** + * Constructor + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Adapter $adapter Database Adapter + */ + public function __construct( Adapter $adapter ) + { + $this->typeno_sqltype = array( + CUBRID::C_DATATYPE_INTEGER => ' INTEGER ', + CUBRID::C_DATATYPE_DOUBLE => ' DOUBLE ', + CUBRID::C_DATATYPE_STRING => ' STRING ', + CUBRID::C_DATATYPE_SPECIAL_DATE => ' DATE ', + CUBRID::C_DATATYPE_SPECIAL_DATETIME => ' DATETIME ', + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( ( $v ) )] = $k; + } + + $this->sqltype_typeno['STRING(1073741823)'] = self::C_DATATYPE_STRING; + + $this->adapter = $adapter; + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer + */ + public function getTypeForID() + { + return self::C_DATATYPE_INTEGER; + } + + /** + * @see QueryWriter::getTables + */ + public function getTables() + { + $rows = $this->adapter->getCol( "SELECT class_name FROM db_class WHERE is_system_class = 'NO';" ); + + return $rows; + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $table ) + { + $sql = 'CREATE TABLE ' + . $this->esc( $table ) + . ' ("id" integer AUTO_INCREMENT, CONSTRAINT "pk_' + . $this->esc( $table, TRUE ) + . '_id" PRIMARY KEY("id"))'; + + $this->adapter->exec( $sql ); + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $table = $this->esc( $table ); + + $columnsRaw = $this->adapter->get( "SHOW COLUMNS FROM $table" ); + + $columns = array(); + foreach ( $columnsRaw as $r ) { + $columns[$r['Field']] = $r['Type']; + } + + return $columns; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + $this->svalue = $value; + + if ( is_null( $value ) ) { + return self::C_DATATYPE_INTEGER; + } + + if ( $flagSpecial ) { + if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) { + return self::C_DATATYPE_SPECIAL_DATE; + } + if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d$/', $value ) ) { + return self::C_DATATYPE_SPECIAL_DATETIME; + } + } + + $value = strval( $value ); + + if ( !$this->startsWithZeros( $value ) ) { + if ( is_numeric( $value ) && ( floor( $value ) == $value ) && $value >= -2147483647 && $value <= 2147483647 ) { + return self::C_DATATYPE_INTEGER; + } + if ( is_numeric( $value ) ) { + return self::C_DATATYPE_DOUBLE; + } + } + + return self::C_DATATYPE_STRING; + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + $r = ( ( isset( $this->sqltype_typeno[$typedescription] ) ) ? $this->sqltype_typeno[$typedescription] : self::C_DATATYPE_SPECIFIED ); + + if ( $includeSpecials ) { + return $r; + } + + if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) { + return self::C_DATATYPE_SPECIFIED; + } + + return $r; + } + + /** + * @see QueryWriter::addColumn + */ + public function addColumn( $type, $column, $field ) + { + $table = $type; + $type = $field; + + $table = $this->esc( $table ); + $column = $this->esc( $column ); + + $type = array_key_exists( $type, $this->typeno_sqltype ) ? $this->typeno_sqltype[$type] : ''; + + $this->adapter->exec( "ALTER TABLE $table ADD COLUMN $column $type " ); + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $columns = array(); + foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); + $table = $this->esc( $type ); + sort( $columns ); // else we get multiple indexes due to order-effects + $name = 'UQ_' . sha1( implode( ',', $columns ) ); + $sql = "ALTER TABLE $table ADD CONSTRAINT UNIQUE $name (" . implode( ',', $columns ) . ")"; + try { + $this->adapter->exec( $sql ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + return ( $state == 'HY000' ) ? ( count( array_diff( array( + QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE + ), $list ) ) !== 3 ) : FALSE; + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $column ) + { + try { + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $column ); + $this->adapter->exec( "CREATE INDEX $name ON $table ($column) " ); + return TRUE; + } catch ( SQLException $e ) { + return FALSE; + } + } + + /** + * @see QueryWriter::addFK + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE ) + { + return $this->buildFK( $type, $targetType, $property, $targetProperty, $isDependent ); + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + foreach ( $this->getTables() as $t ) { + foreach ( $this->getKeyMapForType( $t ) as $k ) { + $this->adapter->exec( "ALTER TABLE \"$t\" DROP FOREIGN KEY \"{$k['name']}\"" ); + } + } + foreach ( $this->getTables() as $t ) { + $this->adapter->exec( "DROP TABLE \"$t\"" ); + } + } + + /** + * @see QueryWriter::esc + */ + public function esc( $dbStructure, $noQuotes = FALSE ) + { + return parent::esc( strtolower( $dbStructure ), $noQuotes ); + } +} +} + +namespace RedBeanPHP { + +/** + * RedBean\Exception Base. + * Represents the base class for RedBeanPHP\Exceptions. + * + * @file RedBeanPHP/Exception.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class RedException extends \Exception +{ +} +} + +namespace RedBeanPHP\RedException { + +use RedBeanPHP\RedException as RedException; + +/** + * SQL Exception. + * Represents a generic database exception independent of the underlying driver. + * + * @file RedBeanPHP/RedException/SQL.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SQL extends RedException +{ + /** + * @var string + */ + private $sqlState; + + /** + * @var array + */ + private $driverDetails = array(); + + /** + * @return array + */ + public function getDriverDetails() + { + return $this->driverDetails; + } + + /** + * @param array $driverDetails + */ + public function setDriverDetails($driverDetails) + { + $this->driverDetails = $driverDetails; + } + + /** + * Returns an ANSI-92 compliant SQL state. + * + * @return string + */ + public function getSQLState() + { + return $this->sqlState; + } + + /** + * Returns the raw SQL STATE, possibly compliant with + * ANSI SQL error codes - but this depends on database driver. + * + * @param string $sqlState SQL state error code + * + * @return void + */ + public function setSQLState( $sqlState ) + { + $this->sqlState = $sqlState; + } + + /** + * To String prints both code and SQL state. + * + * @return string + */ + public function __toString() + { + return '[' . $this->getSQLState() . '] - ' . $this->getMessage()."\n". + 'trace: ' . $this->getTraceAsString(); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\RedException\SQL as SQLException; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\Cursor as Cursor; +use RedBeanPHP\Cursor\NullCursor as NullCursor; + +/** + * Abstract Repository. + * + * OODB manages two repositories, a fluid one that + * adjust the database schema on-the-fly to accomodate for + * new bean types (tables) and new properties (columns) and + * a frozen one for use in a production environment. OODB + * allows you to swap the repository instances using the freeze() + * method. + * + * @file RedBeanPHP/Repository.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +abstract class Repository +{ + /** + * @var array + */ + protected $stash = NULL; + + /* + * @var integer + */ + protected $nesting = 0; + + /** + * @var DBAdapter + */ + protected $writer; + + /** + * @var boolean + */ + protected $partialBeans = FALSE; + + /** + * Toggles 'partial bean mode'. If this mode has been + * selected the repository will only update the fields of a bean that + * have been changed rather than the entire bean. + * Pass the value TRUE to select 'partial mode' for all beans. + * Pass the value FALSE to disable 'partial mode'. + * Pass an array of bean types if you wish to use partial mode only + * for some types. + * This method will return the previous value. + * + * @param boolean|array $yesNoBeans List of type names or 'all' + * + * @return mixed + */ + public function usePartialBeans( $yesNoBeans ) + { + $oldValue = $this->partialBeans; + $this->partialBeans = $yesNoBeans; + return $oldValue; + } + + /** + * Fully processes a bean and updates the associated records in the database. + * First the bean properties will be grouped as 'embedded' bean, + * addition, deleted 'trash can' or residue. Next, the different groups + * of beans will be processed accordingly and the reference bean (i.e. + * the one that was passed to the method as an argument) will be stored. + * Each type of list (own/shared) has 3 bean processors: + * + * - trashCanProcessor : removes the bean or breaks its association with the current bean + * - additionProcessor : associates the bean with the current one + * - residueProcessor : manages beans in lists that 'remain' but may need to be updated + * + * This method first groups the beans and then calls the + * internal processing methods. + * + * @param OODBBean $bean bean to process + * + * @return void + */ + protected function storeBeanWithLists( OODBBean $bean ) + { + $sharedAdditions = $sharedTrashcan = $sharedresidue = $sharedItems = $ownAdditions = $ownTrashcan = $ownresidue = $embeddedBeans = array(); //Define groups + foreach ( $bean as $property => $value ) { + $value = ( $value instanceof SimpleModel ) ? $value->unbox() : $value; + if ( $value instanceof OODBBean ) { + $this->processEmbeddedBean( $embeddedBeans, $bean, $property, $value ); + $bean->setMeta("sys.typeof.{$property}", $value->getMeta('type')); + } elseif ( is_array( $value ) ) { + foreach($value as &$item) { + $item = ( $item instanceof SimpleModel ) ? $item->unbox() : $item; + } + $originals = $bean->moveMeta( 'sys.shadow.' . $property, array() ); + if ( strpos( $property, 'own' ) === 0 ) { + list( $ownAdditions, $ownTrashcan, $ownresidue ) = $this->processGroups( $originals, $value, $ownAdditions, $ownTrashcan, $ownresidue ); + $listName = lcfirst( substr( $property, 3 ) ); + if ($bean->moveMeta( 'sys.exclusive-'. $listName ) ) { + OODBBean::setMetaAll( $ownTrashcan, 'sys.garbage', TRUE ); + OODBBean::setMetaAll( $ownAdditions, 'sys.buildcommand.fkdependson', $bean->getMeta( 'type' ) ); + } + unset( $bean->$property ); + } elseif ( strpos( $property, 'shared' ) === 0 ) { + list( $sharedAdditions, $sharedTrashcan, $sharedresidue ) = $this->processGroups( $originals, $value, $sharedAdditions, $sharedTrashcan, $sharedresidue ); + unset( $bean->$property ); + } + } + } + $this->storeBean( $bean ); + $this->processTrashcan( $bean, $ownTrashcan ); + $this->processAdditions( $bean, $ownAdditions ); + $this->processResidue( $ownresidue ); + $this->processSharedTrashcan( $bean, $sharedTrashcan ); + $this->processSharedAdditions( $bean, $sharedAdditions ); + $this->processSharedResidue( $bean, $sharedresidue ); + } + + /** + * Process groups. Internal function. Processes different kind of groups for + * storage function. Given a list of original beans and a list of current beans, + * this function calculates which beans remain in the list (residue), which + * have been deleted (are in the trashcan) and which beans have been added + * (additions). + * + * @param array $originals originals + * @param array $current the current beans + * @param array $additions beans that have been added + * @param array $trashcan beans that have been deleted + * @param array $residue beans that have been left untouched + * + * @return array + */ + protected function processGroups( $originals, $current, $additions, $trashcan, $residue ) + { + return array( + array_merge( $additions, array_diff( $current, $originals ) ), + array_merge( $trashcan, array_diff( $originals, $current ) ), + array_merge( $residue, array_intersect( $current, $originals ) ) + ); + } + + /** + * Processes a list of beans from a bean. + * A bean may contain lists. This + * method handles shared addition lists; i.e. + * the $bean->sharedObject properties. + * Shared beans will be associated with eachother using the + * Association Manager. + * + * @param OODBBean $bean the bean + * @param array $sharedAdditions list with shared additions + * + * @return void + */ + protected function processSharedAdditions( $bean, $sharedAdditions ) + { + foreach ( $sharedAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + $this->oodb->getAssociationManager()->associate( $addition, $bean ); + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Processes a list of beans from a bean. + * A bean may contain lists. This + * method handles own lists; i.e. + * the $bean->ownObject properties. + * A residue is a bean in an own-list that stays + * where it is. This method checks if there have been any + * modification to this bean, in that case + * the bean is stored once again, otherwise the bean will be left untouched. + * + * @param array $ownresidue list to process + * + * @return void + */ + protected function processResidue( $ownresidue ) + { + foreach ( $ownresidue as $residue ) { + if ( $residue->getMeta( 'tainted' ) ) { + $this->store( $residue ); + } + } + } + + /** + * Processes a list of beans from a bean. A bean may contain lists. This + * method handles own lists; i.e. the $bean->ownObject properties. + * A trash can bean is a bean in an own-list that has been removed + * (when checked with the shadow). This method + * checks if the bean is also in the dependency list. If it is the bean will be removed. + * If not, the connection between the bean and the owner bean will be broken by + * setting the ID to NULL. + * + * @param OODBBean $bean bean to process + * @param array $ownTrashcan list to process + * + * @return void + */ + protected function processTrashcan( $bean, $ownTrashcan ) + { + foreach ( $ownTrashcan as $trash ) { + + $myFieldLink = $bean->getMeta( 'type' ) . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $trash->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + if ( $trash->getMeta( 'sys.garbage' ) === TRUE ) { + $this->trash( $trash ); + } else { + $trash->$myFieldLink = NULL; + $this->store( $trash ); + } + } + } + + /** + * Unassociates the list items in the trashcan. + * This bean processor processes the beans in the shared trash can. + * This group of beans has been deleted from a shared list. + * The affected beans will no longer be associated with the bean + * that contains the shared list. + * + * @param OODBBean $bean bean to process + * @param array $sharedTrashcan list to process + * + * @return void + */ + protected function processSharedTrashcan( $bean, $sharedTrashcan ) + { + foreach ( $sharedTrashcan as $trash ) { + $this->oodb->getAssociationManager()->unassociate( $trash, $bean ); + } + } + + /** + * Stores all the beans in the residue group. + * This bean processor processes the beans in the shared residue + * group. This group of beans 'remains' in the list but might need + * to be updated or synced. The affected beans will be stored + * to perform the required database queries. + * + * @param OODBBean $bean bean to process + * @param array $sharedresidue list to process + * + * @return void + */ + protected function processSharedResidue( $bean, $sharedresidue ) + { + foreach ( $sharedresidue as $residue ) { + $this->store( $residue ); + } + } + + /** + * Determines whether the bean has 'loaded lists' or + * 'loaded embedded beans' that need to be processed + * by the store() method. + * + * @param OODBBean $bean bean to be examined + * + * @return boolean + */ + protected function hasListsOrObjects( OODBBean $bean ) + { + $processLists = FALSE; + foreach ( $bean as $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + $processLists = TRUE; + break; + } + } + + return $processLists; + } + + /** + * Converts an embedded bean to an ID, removes the bean property and + * stores the bean in the embedded beans array. The id will be + * assigned to the link field property, i.e. 'bean_id'. + * + * @param array $embeddedBeans destination array for embedded bean + * @param OODBBean $bean target bean to process + * @param string $property property that contains the embedded bean + * @param OODBBean $value embedded bean itself + * + * @return void + */ + protected function processEmbeddedBean( &$embeddedBeans, $bean, $property, OODBBean $value ) + { + $linkField = $property . '_id'; + if ( !$value->id || $value->getMeta( 'tainted' ) ) { + $this->store( $value ); + } + $id = $value->id; + if ($bean->$linkField != $id) $bean->$linkField = $id; + $bean->setMeta( 'cast.' . $linkField, 'id' ); + $embeddedBeans[$linkField] = $value; + unset( $bean->$property ); + } + + /** + * Constructor, requires a query writer and OODB. + * Creates a new instance of the bean respository class. + * + * @param OODB $oodb instance of object database + * @param QueryWriter $writer the Query Writer to use for this repository + * + * @return void + */ + public function __construct( OODB $oodb, QueryWriter $writer ) + { + $this->writer = $writer; + $this->oodb = $oodb; + } + + /** + * Checks whether a OODBBean bean is valid. + * If the type is not valid or the ID is not valid it will + * throw an exception: Security. To be valid a bean + * must abide to the following rules: + * + * - It must have an primary key id property named: id + * - It must have a type + * - The type must conform to the RedBeanPHP naming policy + * - All properties must be valid + * - All values must be valid + * + * @param OODBBean $bean the bean that needs to be checked + * + * @return void + */ + public function check( OODBBean $bean ) + { + //Is all meta information present? + if ( !isset( $bean->id ) ) { + throw new RedException( 'Bean has incomplete Meta Information id ' ); + } + if ( !( $bean->getMeta( 'type' ) ) ) { + throw new RedException( 'Bean has incomplete Meta Information II' ); + } + //Pattern of allowed characters + $pattern = '/[^a-z0-9_]/i'; + //Does the type contain invalid characters? + if ( preg_match( $pattern, $bean->getMeta( 'type' ) ) ) { + throw new RedException( 'Bean Type is invalid' ); + } + //Are the properties and values valid? + foreach ( $bean as $prop => $value ) { + if ( + is_array( $value ) + || ( is_object( $value ) ) + ) { + throw new RedException( "Invalid Bean value: property $prop" ); + } else if ( + strlen( $prop ) < 1 + || preg_match( $pattern, $prop ) + ) { + throw new RedException( "Invalid Bean property: property $prop" ); + } + } + } + + /** + * Dispenses a new bean (a OODBBean Bean Object) + * of the specified type. Always + * use this function to get an empty bean object. Never + * instantiate a OODBBean yourself because it needs + * to be configured before you can use it with RedBean. This + * function applies the appropriate initialization / + * configuration for you. + * + * To use a different class for beans (instead of OODBBean) set: + * REDBEAN_OODBBEAN_CLASS to the name of the class to be used. + * + * @param string $type type of bean you want to dispense + * @param int $number number of beans you would like to get + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return OODBBean + */ + public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE ) + { + $OODBBEAN = defined( 'REDBEAN_OODBBEAN_CLASS' ) ? REDBEAN_OODBBEAN_CLASS : '\RedBeanPHP\OODBBean'; + $beans = array(); + for ( $i = 0; $i < $number; $i++ ) { + $bean = new $OODBBEAN; + $bean->initializeForDispense( $type, $this->oodb->getBeanHelper() ); + $this->check( $bean ); + $this->oodb->signal( 'dispense', $bean ); + $beans[] = $bean; + } + + return ( count( $beans ) === 1 && !$alwaysReturnArray ) ? array_pop( $beans ) : $beans; + } + + /** + * Searches the database for a bean that matches conditions $conditions and sql $addSQL + * and returns an array containing all the beans that have been found. + * + * Conditions need to take form: + * + * + * array( + * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' ) + * 'PROPERTY' => array( POSSIBLE VALUES... ) + * ); + * + * + * All conditions are glued together using the AND-operator, while all value lists + * are glued using IN-operators thus acting as OR-conditions. + * + * Note that you can use property names; the columns will be extracted using the + * appropriate bean formatter. + * + * @param string $type type of beans you are looking for + * @param array $conditions list of conditions + * @param string $sql SQL to be used in query + * @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not) + * + * @return array + */ + public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() ) + { + //for backward compatibility, allow mismatch arguments: + if ( is_array( $sql ) ) { + if ( isset( $sql[1] ) ) { + $bindings = $sql[1]; + } + $sql = $sql[0]; + } + try { + $beans = $this->convertToBeans( $type, $this->writer->queryRecord( $type, $conditions, $sql, $bindings ) ); + + return $beans; + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + + return array(); + } + + /** + * Finds a BeanCollection. + * Given a type, an SQL snippet and optionally some parameter bindings + * this methods returns a BeanCollection for your query. + * + * The BeanCollection represents a collection of beans and + * makes it possible to use database cursors. The BeanCollection + * has a method next() to obtain the first, next and last bean + * in the collection. The BeanCollection does not implement the array + * interface nor does it try to act like an array because it cannot go + * backward or rewind itself. + * + * @param string $type type of beans you are looking for + * @param string $sql SQL to be used in query + * @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not) + * + * @return BeanCollection + */ + public function findCollection( $type, $sql, $bindings = array() ) + { + try { + $cursor = $this->writer->queryRecordWithCursor( $type, $sql, $bindings ); + return new BeanCollection( $type, $this, $cursor ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + return new BeanCollection( $type, $this, new NullCursor ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. We use + * explicit casts instead of functions to preserve performance + * (0.13 vs 0.28 for 10000 iterations on Core i3). + * + * @param OODBBean|SimpleModel $bean bean to store + * + * @return integer|string + */ + public function store( $bean ) + { + $processLists = $this->hasListsOrObjects( $bean ); + if ( !$processLists && !$bean->getMeta( 'tainted' ) ) { + return $bean->getID(); //bail out! + } + $this->oodb->signal( 'update', $bean ); + $processLists = $this->hasListsOrObjects( $bean ); //check again, might have changed by model! + if ( $processLists ) { + $this->storeBeanWithLists( $bean ); + } else { + $this->storeBean( $bean ); + } + $this->oodb->signal( 'after_update', $bean ); + + return ( (string) $bean->id === (string) (int) $bean->id ) ? (int) $bean->id : (string) $bean->id; + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public function batch( $type, $ids ) + { + if ( !$ids ) { + return array(); + } + $collection = array(); + try { + $rows = $this->writer->queryRecord( $type, array( 'id' => $ids ) ); + } catch ( SQLException $e ) { + $this->handleException( $e ); + $rows = FALSE; + } + $this->stash[$this->nesting] = array(); + if ( !$rows ) { + return array(); + } + foreach ( $rows as $row ) { + $this->stash[$this->nesting][$row['id']] = $row; + } + foreach ( $ids as $id ) { + $collection[$id] = $this->load( $type, $id ); + } + $this->stash[$this->nesting] = NULL; + + return $collection; + } + + /** + * This is a convenience method; it converts database rows + * (arrays) into beans. Given a type and a set of rows this method + * will return an array of beans of the specified type loaded with + * the data fields provided by the result set from the database. + * + * New in 4.3.2: meta mask. The meta mask is a special mask to send + * data from raw result rows to the meta store of the bean. This is + * useful for bundling additional information with custom queries. + * Values of every column whos name starts with $mask will be + * transferred to the meta section of the bean under key 'data.bundle'. + * + * @param string $type type of beans you would like to have + * @param array $rows rows from the database result + * @param string $mask meta mask to apply (optional) + * + * @return array + */ + public function convertToBeans( $type, $rows, $mask = '__meta' ) + { + $masktype = gettype( $mask ); + switch ( $masktype ) { + case 'string': + break; + case 'array': + $maskflip = array(); + foreach ( $mask as $m ) { + if ( !is_string( $m ) ) { + $mask = NULL; + $masktype = 'NULL'; + break 2; + } + $maskflip[$m] = TRUE; + } + $mask = $maskflip; + break; + default: + $mask = NULL; + $masktype = 'NULL'; + } + + $collection = array(); + $this->stash[$this->nesting] = array(); + foreach ( $rows as $row ) { + if ( $mask !== NULL ) { + $meta = array(); + foreach( $row as $key => $value ) { + if ( $masktype === 'string' ) { + if ( strpos( $key, $mask ) === 0 ) { + unset( $row[$key] ); + $meta[$key] = $value; + } + } elseif ( $masktype === 'array' ) { + if ( isset( $mask[$key] ) ) { + unset( $row[$key] ); + $meta[$key] = $value; + } + } + } + } + + $id = $row['id']; + $this->stash[$this->nesting][$id] = $row; + $collection[$id] = $this->load( $type, $id ); + + if ( $mask !== NULL ) { + $collection[$id]->setMeta( 'data.bundle', $meta ); + } + } + $this->stash[$this->nesting] = NULL; + + return $collection; + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public function count( $type, $addSQL = '', $bindings = array() ) + { + $type = AQueryWriter::camelsSnake( $type ); + if ( count( explode( '_', $type ) ) > 2 ) { + throw new RedException( 'Invalid type for count.' ); + } + + try { + $count = (int) $this->writer->queryRecordCount( $type, array(), $addSQL, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + $count = 0; + } + return $count; + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * @param OODBBean|SimpleModel $bean bean you want to remove from database + * + * @return void + */ + public function trash( $bean ) + { + $this->oodb->signal( 'delete', $bean ); + foreach ( $bean as $property => $value ) { + if ( $value instanceof OODBBean ) { + unset( $bean->$property ); + } + if ( is_array( $value ) ) { + if ( strpos( $property, 'own' ) === 0 ) { + unset( $bean->$property ); + } elseif ( strpos( $property, 'shared' ) === 0 ) { + unset( $bean->$property ); + } + } + } + try { + $deleted = $this->writer->deleteRecord( $bean->getMeta( 'type' ), array( 'id' => array( $bean->id ) ), NULL ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + $bean->id = 0; + $this->oodb->signal( 'after_delete', $bean ); + return isset($deleted) ? $deleted : 0; + } + + /** + * Checks whether the specified table already exists in the database. + * Not part of the Object Database interface! + * + * @deprecated Use AQueryWriter::typeExists() instead. + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + return $this->writer->tableExists( $table ); + } + + /** + * Trash all beans of a given type. + * Wipes an entire type of bean. After this operation there + * will be no beans left of the specified type. + * This method will ignore exceptions caused by database + * tables that do not exist. + * + * @param string $type type of bean you wish to delete all instances of + * + * @return boolean + */ + public function wipe( $type ) + { + try { + $this->writer->wipe( $type ); + + return TRUE; + } catch ( SQLException $exception ) { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ), $exception->getDriverDetails() ) ) { + throw $exception; + } + + return FALSE; + } + } +} +} + +namespace RedBeanPHP\Repository { + +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\RedException\SQL as SQLException; +use RedBeanPHP\Repository as Repository; + +/** + * Fluid Repository. + * OODB manages two repositories, a fluid one that + * adjust the database schema on-the-fly to accomodate for + * new bean types (tables) and new properties (columns) and + * a frozen one for use in a production environment. OODB + * allows you to swap the repository instances using the freeze() + * method. + * + * @file RedBeanPHP/Repository/Fluid.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Fluid extends Repository +{ + /** + * Figures out the desired type given the cast string ID. + * Given a cast ID, this method will return the associated + * type (INT(10) or VARCHAR for instance). The returned type + * can be processed by the Query Writer to build the specified + * column for you in the database. The Cast ID is actually just + * a superset of the QueryWriter types. In addition to default + * Query Writer column types you can pass the following 'cast types': + * 'id' and 'string'. These will map to Query Writer specific + * column types (probably INT and VARCHAR). + * + * @param string $cast cast identifier + * + * @return integer + */ + private function getTypeFromCast( $cast ) + { + if ( $cast == 'string' ) { + $typeno = $this->writer->scanType( 'STRING' ); + } elseif ( $cast == 'id' ) { + $typeno = $this->writer->getTypeForID(); + } elseif ( isset( $this->writer->sqltype_typeno[$cast] ) ) { + $typeno = $this->writer->sqltype_typeno[$cast]; + } else { + throw new RedException( 'Invalid Cast' ); + } + + return $typeno; + } + + /** + * Orders the Query Writer to create a table if it does not exist already and + * adds a note in the build report about the creation. + * + * @param OODBBean $bean bean to update report of + * @param string $table table to check and create if not exists + * + * @return void + */ + private function createTableIfNotExists( OODBBean $bean, $table ) + { + //Does table exist? If not, create + if ( !$this->tableExists( $this->writer->esc( $table, TRUE ) ) ) { + $this->writer->createTable( $table ); + $bean->setMeta( 'buildreport.flags.created', TRUE ); + } + } + + /** + * Modifies the table to fit the bean data. + * Given a property and a value and the bean, this method will + * adjust the table structure to fit the requirements of the property and value. + * This may include adding a new column or widening an existing column to hold a larger + * or different kind of value. This method employs the writer to adjust the table + * structure in the database. Schema updates are recorded in meta properties of the bean. + * + * This method will also apply indexes, unique constraints and foreign keys. + * + * @param OODBBean $bean bean to get cast data from and store meta in + * @param string $property property to store + * @param mixed $value value to store + * + * @return void + */ + private function modifySchema( OODBBean $bean, $property, $value, &$columns = NULL ) + { + $doFKStuff = FALSE; + $table = $bean->getMeta( 'type' ); + if ($columns === NULL) { + $columns = $this->writer->getColumns( $table ); + } + $columnNoQ = $this->writer->esc( $property, TRUE ); + if ( !$this->oodb->isChilled( $bean->getMeta( 'type' ) ) ) { + if ( $bean->getMeta( "cast.$property", -1 ) !== -1 ) { //check for explicitly specified types + $cast = $bean->getMeta( "cast.$property" ); + $typeno = $this->getTypeFromCast( $cast ); + } else { + $cast = FALSE; + $typeno = $this->writer->scanType( $value, TRUE ); + } + if ( isset( $columns[$this->writer->esc( $property, TRUE )] ) ) { //Is this property represented in the table ? + if ( !$cast ) { //rescan without taking into account special types >80 + $typeno = $this->writer->scanType( $value, FALSE ); + } + $sqlt = $this->writer->code( $columns[$this->writer->esc( $property, TRUE )] ); + if ( $typeno > $sqlt ) { //no, we have to widen the database column type + $this->writer->widenColumn( $table, $property, $typeno ); + $bean->setMeta( 'buildreport.flags.widen', TRUE ); + $doFKStuff = TRUE; + } + } else { + $this->writer->addColumn( $table, $property, $typeno ); + $bean->setMeta( 'buildreport.flags.addcolumn', TRUE ); + $doFKStuff = TRUE; + } + if ($doFKStuff) { + if (strrpos($columnNoQ, '_id')===(strlen($columnNoQ)-3)) { + $destinationColumnNoQ = substr($columnNoQ, 0, strlen($columnNoQ)-3); + $indexName = "index_foreignkey_{$table}_{$destinationColumnNoQ}"; + $this->writer->addIndex($table, $indexName, $columnNoQ); + $typeof = $bean->getMeta("sys.typeof.{$destinationColumnNoQ}", $destinationColumnNoQ); + $isLink = $bean->getMeta( 'sys.buildcommand.unique', FALSE ); + //Make FK CASCADING if part of exclusive list (dependson=typeof) or if link bean + $isDep = ( $bean->moveMeta( 'sys.buildcommand.fkdependson' ) === $typeof || is_array( $isLink ) ); + $result = $this->writer->addFK( $table, $typeof, $columnNoQ, 'id', $isDep ); + //If this is a link bean and all unique columns have been added already, then apply unique constraint + if ( is_array( $isLink ) && !count( array_diff( $isLink, array_keys( $this->writer->getColumns( $table ) ) ) ) ) { + $this->writer->addUniqueConstraint( $table, $bean->moveMeta('sys.buildcommand.unique') ); + $bean->setMeta("sys.typeof.{$destinationColumnNoQ}", NULL); + } + } + } + } + } + + /** + * Part of the store() functionality. + * Handles all new additions after the bean has been saved. + * Stores addition bean in own-list, extracts the id and + * adds a foreign key. Also adds a constraint in case the type is + * in the dependent list. + * + * Note that this method raises a custom exception if the bean + * is not an instance of OODBBean. Therefore it does not use + * a type hint. This allows the user to take action in case + * invalid objects are passed in the list. + * + * @param OODBBean $bean bean to process + * @param array $ownAdditions list of addition beans in own-list + * + * @return void + */ + protected function processAdditions( $bean, $ownAdditions ) + { + $beanType = $bean->getMeta( 'type' ); + + foreach ( $ownAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + + $myFieldLink = $beanType . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + $addition->$myFieldLink = $bean->id; + $addition->setMeta( 'cast.' . $myFieldLink, 'id' ); + + if ($alias) { + $addition->setMeta( "sys.typeof.{$alias}", $beanType ); + } else { + $addition->setMeta( "sys.typeof.{$beanType}", $beanType ); + } + + $this->store( $addition ); + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Stores a cleaned bean; i.e. only scalar values. This is the core of the store() + * method. When all lists and embedded beans (parent objects) have been processed and + * removed from the original bean the bean is passed to this method to be stored + * in the database. + * + * @param OODBBean $bean the clean bean + * + * @return void + */ + protected function storeBean( OODBBean $bean ) + { + if ( $bean->getMeta( 'changed' ) ) { + $this->check( $bean ); + $table = $bean->getMeta( 'type' ); + $this->createTableIfNotExists( $bean, $table ); + + $updateValues = array(); + + $partial = ( $this->partialBeans === TRUE || ( is_array( $this->partialBeans ) && in_array( $table, $this->partialBeans ) ) ); + if ( $partial ) { + $mask = $bean->getMeta( 'changelist' ); + $bean->setMeta( 'changelist', array() ); + } + + $columnCache = NULL; + foreach ( $bean as $property => $value ) { + if ( $partial && !in_array( $property, $mask ) ) continue; + if ( $property !== 'id' ) { + $this->modifySchema( $bean, $property, $value, $columnCache ); + } + if ( $property !== 'id' ) { + $updateValues[] = array( 'property' => $property, 'value' => $value ); + } + } + + $bean->id = $this->writer->updateRecord( $table, $updateValues, $bean->id ); + $bean->setMeta( 'changed', FALSE ); + } + $bean->setMeta( 'tainted', FALSE ); + } + + /** + * Exception handler. + * Fluid and Frozen mode have different ways of handling + * exceptions. Fluid mode (using the fluid repository) ignores + * exceptions caused by the following: + * + * - missing tables + * - missing column + * + * In these situations, the repository will behave as if + * no beans could be found. This is because in fluid mode + * it might happen to query a table or column that has not been + * created yet. In frozen mode, this is not supposed to happen + * and the corresponding exceptions will be thrown. + * + * @param \Exception $exception exception + * + * @return void + */ + protected function handleException( \Exception $exception ) + { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), + $exception->getDriverDetails() ) + ) { + throw $exception; + } + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public function load( $type, $id ) + { + $rows = array(); + $bean = $this->dispense( $type ); + if ( isset( $this->stash[$this->nesting][$id] ) ) { + $row = $this->stash[$this->nesting][$id]; + } else { + try { + $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) ); + } catch ( SQLException $exception ) { + if ( + $this->writer->sqlStateIn( + $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE + ), + $exception->getDriverDetails() + ) + ) { + $rows = array(); + } else { + throw $exception; + } + } + if ( !count( $rows ) ) { + return $bean; + } + $row = array_pop( $rows ); + } + $bean->importRow( $row ); + $this->nesting++; + $this->oodb->signal( 'open', $bean ); + $this->nesting--; + + return $bean->setMeta( 'tainted', FALSE ); + } +} +} + +namespace RedBeanPHP\Repository { + +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\RedException\SQL as SQLException; +use RedBeanPHP\Repository as Repository; + +/** + * Frozen Repository. + * OODB manages two repositories, a fluid one that + * adjust the database schema on-the-fly to accomodate for + * new bean types (tables) and new properties (columns) and + * a frozen one for use in a production environment. OODB + * allows you to swap the repository instances using the freeze() + * method. + * + * @file RedBeanPHP/Repository/Frozen.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Frozen extends Repository +{ + /** + * Exception handler. + * Fluid and Frozen mode have different ways of handling + * exceptions. Fluid mode (using the fluid repository) ignores + * exceptions caused by the following: + * + * - missing tables + * - missing column + * + * In these situations, the repository will behave as if + * no beans could be found. This is because in fluid mode + * it might happen to query a table or column that has not been + * created yet. In frozen mode, this is not supposed to happen + * and the corresponding exceptions will be thrown. + * + * @param \Exception $exception exception + * + * @return void + */ + protected function handleException( \Exception $exception ) + { + throw $exception; + } + + /** + * Stores a cleaned bean; i.e. only scalar values. This is the core of the store() + * method. When all lists and embedded beans (parent objects) have been processed and + * removed from the original bean the bean is passed to this method to be stored + * in the database. + * + * @param OODBBean $bean the clean bean + * + * @return void + */ + protected function storeBean( OODBBean $bean ) + { + if ( $bean->getMeta( 'changed' ) ) { + + list( $properties, $table ) = $bean->getPropertiesAndType(); + $id = $properties['id']; + unset($properties['id']); + $updateValues = array(); + $k1 = 'property'; + $k2 = 'value'; + + $partial = ( $this->partialBeans === TRUE || ( is_array( $this->partialBeans ) && in_array( $table, $this->partialBeans ) ) ); + if ( $partial ) { + $mask = $bean->getMeta( 'changelist' ); + $bean->setMeta( 'changelist', array() ); + } + + foreach( $properties as $key => $value ) { + if ( $partial && !in_array( $key, $mask ) ) continue; + $updateValues[] = array( $k1 => $key, $k2 => $value ); + } + $bean->id = $this->writer->updateRecord( $table, $updateValues, $id ); + $bean->setMeta( 'changed', FALSE ); + } + $bean->setMeta( 'tainted', FALSE ); + } + + /** + * Part of the store() functionality. + * Handles all new additions after the bean has been saved. + * Stores addition bean in own-list, extracts the id and + * adds a foreign key. Also adds a constraint in case the type is + * in the dependent list. + * + * Note that this method raises a custom exception if the bean + * is not an instance of OODBBean. Therefore it does not use + * a type hint. This allows the user to take action in case + * invalid objects are passed in the list. + * + * @param OODBBean $bean bean to process + * @param array $ownAdditions list of addition beans in own-list + * + * @return void + * @throws RedException + */ + protected function processAdditions( $bean, $ownAdditions ) + { + $beanType = $bean->getMeta( 'type' ); + + $cachedIndex = array(); + foreach ( $ownAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + + $myFieldLink = $beanType . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + $addition->$myFieldLink = $bean->id; + $addition->setMeta( 'cast.' . $myFieldLink, 'id' ); + $this->store( $addition ); + + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + * @throws SQLException + */ + public function load( $type, $id ) + { + $rows = array(); + $bean = $this->dispense( $type ); + if ( isset( $this->stash[$this->nesting][$id] ) ) { + $row = $this->stash[$this->nesting][$id]; + } else { + $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) ); + if ( !count( $rows ) ) { + return $bean; + } + $row = array_pop( $rows ); + } + $bean->importRow( $row ); + $this->nesting++; + $this->oodb->signal( 'open', $bean ); + $this->nesting--; + + return $bean->setMeta( 'tainted', FALSE ); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\Repository as Repository; +use RedBeanPHP\Repository\Fluid as FluidRepo; +use RedBeanPHP\Repository\Frozen as FrozenRepo; + +/** + * RedBean Object Oriented DataBase. + * + * The RedBean OODB Class is the main class of RedBeanPHP. + * It takes OODBBean objects and stores them to and loads them from the + * database as well as providing other CRUD functions. This class acts as a + * object database. + * + * @file RedBeanPHP/OODB.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class OODB extends Observable +{ + /** + * @var array + */ + private static $sqlFilters = array(); + + /** + * @var array + */ + protected $chillList = array(); + + /** + * @var array + */ + protected $stash = NULL; + + /* + * @var integer + */ + protected $nesting = 0; + + /** + * @var DBAdapter + */ + protected $writer; + + /** + * @var boolean + */ + protected $isFrozen = FALSE; + + /** + * @var FacadeBeanHelper + */ + protected $beanhelper = NULL; + + /** + * @var AssociationManager + */ + protected $assocManager = NULL; + + /** + * @var Repository + */ + protected $repository = NULL; + + /** + * @var FrozenRepo + */ + protected $frozenRepository = NULL; + + /** + * @var FluidRepo + */ + protected $fluidRepository = NULL; + + /** + * @var boolean + */ + protected static $autoClearHistoryAfterStore = FALSE; + + /** + * If set to TRUE, this method will call clearHistory every time + * the bean gets stored. + * + * @param boolean $autoClear auto clear option + * + * @return void + */ + public static function autoClearHistoryAfterStore( $autoClear = TRUE ) + { + self::$autoClearHistoryAfterStore = (boolean) $autoClear; + } + + /** + * Unboxes a bean from a FUSE model if needed and checks whether the bean is + * an instance of OODBBean. + * + * @param OODBBean $bean bean you wish to unbox + * + * @return OODBBean + */ + protected function unboxIfNeeded( $bean ) + { + if ( $bean instanceof SimpleModel ) { + $bean = $bean->unbox(); + } + if ( !( $bean instanceof OODBBean ) ) { + throw new RedException( 'OODB Store requires a bean, got: ' . gettype( $bean ) ); + } + + return $bean; + } + + /** + * Constructor, requires a query writer. + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param QueryWriter $writer writer + * @param array|boolean $frozen mode of operation: TRUE (frozen), FALSE (default, fluid) or ARRAY (chilled) + */ + public function __construct( QueryWriter $writer, $frozen = FALSE ) + { + if ( $writer instanceof QueryWriter ) { + $this->writer = $writer; + } + + $this->freeze( $frozen ); + } + + /** + * Toggles fluid or frozen mode. In fluid mode the database + * structure is adjusted to accomodate your objects. In frozen mode + * this is not the case. + * + * You can also pass an array containing a selection of frozen types. + * Let's call this chilly mode, it's just like fluid mode except that + * certain types (i.e. tables) aren't touched. + * + * @param boolean|array $toggle TRUE if you want to use OODB instance in frozen mode + * + * @return void + */ + public function freeze( $toggle ) + { + if ( is_array( $toggle ) ) { + $this->chillList = $toggle; + $this->isFrozen = FALSE; + } else { + $this->isFrozen = (boolean) $toggle; + } + + if ( $this->isFrozen ) { + if ( !$this->frozenRepository ) { + $this->frozenRepository = new FrozenRepo( $this, $this->writer ); + } + + $this->repository = $this->frozenRepository; + + } else { + if ( !$this->fluidRepository ) { + $this->fluidRepository = new FluidRepo( $this, $this->writer ); + } + + $this->repository = $this->fluidRepository; + } + + if ( count( self::$sqlFilters ) ) { + AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) ); + } + + } + + /** + * Returns the current mode of operation of RedBean. + * In fluid mode the database + * structure is adjusted to accomodate your objects. + * In frozen mode + * this is not the case. + * + * @return boolean + */ + public function isFrozen() + { + return (bool) $this->isFrozen; + } + + /** + * Determines whether a type is in the chill list. + * If a type is 'chilled' it's frozen, so its schema cannot be + * changed anymore. However other bean types may still be modified. + * This method is a convenience method for other objects to check if + * the schema of a certain type is locked for modification. + * + * @param string $type the type you wish to check + * + * @return boolean + */ + public function isChilled( $type ) + { + return (boolean) ( in_array( $type, $this->chillList ) ); + } + + /** + * Dispenses a new bean (a OODBBean Bean Object) + * of the specified type. Always + * use this function to get an empty bean object. Never + * instantiate a OODBBean yourself because it needs + * to be configured before you can use it with RedBean. This + * function applies the appropriate initialization / + * configuration for you. + * + * @param string $type type of bean you want to dispense + * @param string $number number of beans you would like to get + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return OODBBean + */ + public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE ) + { + if ( $number < 1 ) { + if ( $alwaysReturnArray ) return array(); + return NULL; + } + + return $this->repository->dispense( $type, $number, $alwaysReturnArray ); + } + + /** + * Sets bean helper to be given to beans. + * Bean helpers assist beans in getting a reference to a toolbox. + * + * @param BeanHelper $beanhelper helper + * + * @return void + */ + public function setBeanHelper( BeanHelper $beanhelper ) + { + $this->beanhelper = $beanhelper; + } + + /** + * Returns the current bean helper. + * Bean helpers assist beans in getting a reference to a toolbox. + * + * @return BeanHelper + */ + public function getBeanHelper() + { + return $this->beanhelper; + } + + /** + * Checks whether a OODBBean bean is valid. + * If the type is not valid or the ID is not valid it will + * throw an exception: Security. + * + * @param OODBBean $bean the bean that needs to be checked + * + * @return void + */ + public function check( OODBBean $bean ) + { + $this->repository->check( $bean ); + } + + /** + * Searches the database for a bean that matches conditions $conditions and sql $addSQL + * and returns an array containing all the beans that have been found. + * + * Conditions need to take form: + * + * + * array( + * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' ) + * 'PROPERTY' => array( POSSIBLE VALUES... ) + * ); + * + * + * All conditions are glued together using the AND-operator, while all value lists + * are glued using IN-operators thus acting as OR-conditions. + * + * Note that you can use property names; the columns will be extracted using the + * appropriate bean formatter. + * + * @param string $type type of beans you are looking for + * @param array $conditions list of conditions + * @param string $sql SQL to be used in query + * @param array $bindings a list of values to bind to query parameters + * + * @return array + */ + public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() ) + { + return $this->repository->find( $type, $conditions, $sql, $bindings ); + } + + /** + * Same as find() but returns a BeanCollection. + * + * @param string $type type of beans you are looking for + * @param string $sql SQL to be used in query + * @param array $bindings a list of values to bind to query parameters + * + * @return BeanCollection + */ + public function findCollection( $type, $sql = NULL, $bindings = array() ) + { + return $this->repository->findCollection( $type, $sql, $bindings ); + } + + /** + * Checks whether the specified table already exists in the database. + * Not part of the Object Database interface! + * + * @deprecated Use AQueryWriter::typeExists() instead. + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + return $this->repository->tableExists( $table ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. We use + * explicit casts instead of functions to preserve performance + * (0.13 vs 0.28 for 10000 iterations on Core i3). + * + * @param OODBBean|SimpleModel $bean bean to store + * + * @return integer|string + */ + public function store( $bean ) + { + $bean = $this->unboxIfNeeded( $bean ); + $id = $this->repository->store( $bean ); + if ( self::$autoClearHistoryAfterStore ) { + $bean->clearHistory(); + } + return $id; + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public function load( $type, $id ) + { + return $this->repository->load( $type, $id ); + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * @param OODBBean|SimpleModel $bean bean you want to remove from database + * + * @return void + */ + public function trash( $bean ) + { + $bean = $this->unboxIfNeeded( $bean ); + return $this->repository->trash( $bean ); + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public function batch( $type, $ids ) + { + return $this->repository->batch( $type, $ids ); + } + + /** + * This is a convenience method; it converts database rows + * (arrays) into beans. Given a type and a set of rows this method + * will return an array of beans of the specified type loaded with + * the data fields provided by the result set from the database. + * + * @param string $type type of beans you would like to have + * @param array $rows rows from the database result + * @param string $mask mask to apply for meta data + * + * @return array + */ + public function convertToBeans( $type, $rows, $mask = NULL ) + { + return $this->repository->convertToBeans( $type, $rows, $mask ); + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public function count( $type, $addSQL = '', $bindings = array() ) + { + return $this->repository->count( $type, $addSQL, $bindings ); + } + + /** + * Trash all beans of a given type. Wipes an entire type of bean. + * + * @param string $type type of bean you wish to delete all instances of + * + * @return boolean + */ + public function wipe( $type ) + { + return $this->repository->wipe( $type ); + } + + /** + * Returns an Association Manager for use with OODB. + * A simple getter function to obtain a reference to the association manager used for + * storage and more. + * + * @return AssociationManager + */ + public function getAssociationManager() + { + if ( !isset( $this->assocManager ) ) { + throw new RedException( 'No association manager available.' ); + } + + return $this->assocManager; + } + + /** + * Sets the association manager instance to be used by this OODB. + * A simple setter function to set the association manager to be used for storage and + * more. + * + * @param AssociationManager $assocManager sets the association manager to be used + * + * @return void + */ + public function setAssociationManager( AssociationManager $assocManager ) + { + $this->assocManager = $assocManager; + } + + /** + * Returns the currently used repository instance. + * For testing purposes only. + * + * @return Repository + */ + public function getCurrentRepository() + { + return $this->repository; + } + + /** + * Clears all function bindings. + * + * @return void + */ + public function clearAllFuncBindings() + { + self::$sqlFilters = array(); + AQueryWriter::setSQLFilters( self::$sqlFilters, FALSE ); + } + + /** + * Binds an SQL function to a column. + * This method can be used to setup a decode/encode scheme or + * perform UUID insertion. This method is especially useful for handling + * MySQL spatial columns, because they need to be processed first using + * the asText/GeomFromText functions. + * + * @param string $mode mode to set function for, i.e. read or write + * @param string $field field (table.column) to bind SQL function to + * @param string $function SQL function to bind to field + * @param boolean $isTemplate TRUE if $function is an SQL string, FALSE for just a function name + * + * @return void + */ + public function bindFunc( $mode, $field, $function, $isTemplate = FALSE ) + { + list( $type, $property ) = explode( '.', $field ); + $mode = ($mode === 'write') ? QueryWriter::C_SQLFILTER_WRITE : QueryWriter::C_SQLFILTER_READ; + + if ( !isset( self::$sqlFilters[$mode] ) ) self::$sqlFilters[$mode] = array(); + if ( !isset( self::$sqlFilters[$mode][$type] ) ) self::$sqlFilters[$mode][$type] = array(); + + if ( is_null( $function ) ) { + unset( self::$sqlFilters[$mode][$type][$property] ); + } else { + if ($mode === QueryWriter::C_SQLFILTER_WRITE) { + if ($isTemplate) { + $code = sprintf( $function, '?' ); + } else { + $code = "{$function}(?)"; + } + self::$sqlFilters[$mode][$type][$property] = $code; + } else { + if ($isTemplate) { + $code = sprintf( $function, $field ); + } else { + $code = "{$function}({$field})"; + } + self::$sqlFilters[$mode][$type][$property] = $code; + } + } + AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) ); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\Adapter as Adapter; + +/** + * ToolBox. + * + * The toolbox is an integral part of RedBeanPHP providing the basic + * architectural building blocks to manager objects, helpers and additional tools + * like plugins. A toolbox contains the three core components of RedBeanPHP: + * the adapter, the query writer and the core functionality of RedBeanPHP in + * OODB. + * + * @file RedBeanPHP/ToolBox.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class ToolBox +{ + /** + * @var OODB + */ + protected $oodb; + + /** + * @var QueryWriter + */ + protected $writer; + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * Constructor. + * The toolbox is an integral part of RedBeanPHP providing the basic + * architectural building blocks to manager objects, helpers and additional tools + * like plugins. A toolbox contains the three core components of RedBeanPHP: + * the adapter, the query writer and the core functionality of RedBeanPHP in + * OODB. + * + * Usage: + * + * + * $toolbox = new ToolBox( $oodb, $adapter, $writer ); + * $plugin = new MyPlugin( $toolbox ); + * + * + * The example above illustrates how the toolbox is used. + * The core objects are passed to the ToolBox constructor to + * assemble a toolbox instance. The toolbox is then passed to + * the plugin, helper or manager object. Instances of + * TagManager, AssociationManager and so on are examples of + * this, they all require a toolbox. The toolbox can also + * be obtained from the facade using: R::getToolBox(); + * + * @param OODB $oodb Object Database, OODB + * @param DBAdapter $adapter Database Adapter + * @param QueryWriter $writer Query Writer + */ + public function __construct( OODB $oodb, Adapter $adapter, QueryWriter $writer ) + { + $this->oodb = $oodb; + $this->adapter = $adapter; + $this->writer = $writer; + return $this; + } + + /** + * Returns the query writer in this toolbox. + * The Query Writer is responsible for building the queries for a + * specific database and executing them through the adapter. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return QueryWriter + */ + public function getWriter() + { + return $this->writer; + } + + /** + * Returns the OODB instance in this toolbox. + * OODB is responsible for creating, storing, retrieving and deleting + * single beans. Other components rely + * on OODB for their basic functionality. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return OODB + */ + public function getRedBean() + { + return $this->oodb; + } + + /** + * Returns the database adapter in this toolbox. + * The adapter is responsible for executing the query and binding the values. + * The adapter also takes care of transaction handling. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return DBAdapter + */ + public function getDatabaseAdapter() + { + return $this->adapter; + } +} +} + +namespace RedBeanPHP { + + +/** + * RedBeanPHP Finder. + * Service class to find beans. For the most part this class + * offers user friendly utility methods for interacting with the + * OODB::find() method, which is rather complex. This class can be + * used to find beans using plain old SQL queries. + * + * @file RedBeanPHP/Finder.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Finder +{ + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * @var OODB + */ + protected $redbean; + + /** + * Constructor. + * The Finder requires a toolbox. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + } + + /** + * A custom record-to-bean mapping function for findMulti. + * + * Usage: + * + * + * $collection = R::findMulti( 'shop,product,price', + * 'SELECT shop.*, product.*, price.* FROM shop + * LEFT JOIN product ON product.shop_id = shop.id + * LEFT JOIN price ON price.product_id = product.id', [], [ + * Finder::map( 'shop', 'product' ), + * Finder::map( 'product', 'price' ), + * ]); + * + * + * @param string $parentName name of the parent bean + * @param string $childName name of the child bean + * + * @return array + */ + public static function map($parentName,$childName) { + return array( + 'a' => $parentName, + 'b' => $childName, + 'matcher' => function( $parent, $child ) use ( $parentName, $childName ) { + $propertyName = 'own' . ucfirst( $childName ); + if (!isset($parent[$propertyName])) { + $parent->noLoad()->{$propertyName} = array(); + } + $property = "{$parentName}ID"; + return ( $child->$property == $parent->id ); + }, + 'do' => function( $parent, $child ) use ( $childName ) { + $list = 'own'.ucfirst( $childName ).'List'; + $parent->noLoad()->{$list}[$child->id] = $child; + } + ); + } + + /** + * A custom record-to-bean mapping function for findMulti. + * + * Usage: + * + * + * $collection = R::findMulti( 'book,book_tag,tag', + * 'SELECT book.*, book_tag.*, tag.* FROM book + * LEFT JOIN book_tag ON book_tag.book_id = book.id + * LEFT JOIN tag ON book_tag.tag_id = tag.id', [], [ + * Finder::nmMap( 'book', 'tag' ), + * ]); + * + * + * @param string $parentName name of the parent bean + * @param string $childName name of the child bean + * + * @return array + */ + public static function nmMap( $parentName, $childName ) + { + $types = array($parentName, $childName); + sort( $types ); + $link = implode( '_', $types ); + return array( + 'a' => $parentName, + 'b' => $childName, + 'matcher' => function( $parent, $child, $beans ) use ( $parentName, $childName, $link ) { + $propertyName = 'shared' . ucfirst( $childName ); + if (!isset($parent[$propertyName])) { + $parent->noLoad()->{$propertyName} = array(); + } + foreach( $beans[$link] as $linkBean ) { + if ( $linkBean["{$parentName}ID"] == $parent->id && $linkBean["{$childName}ID"] == $child->id ) { + return true; + } + } + }, + 'do' => function( $parent, $child ) use ( $childName ) { + $list = 'shared'.ucfirst( $childName ).'List'; + $parent->noLoad()->{$list}[$child->id] = $child; + } + ); + } + + /** + * Finder::onMap() -> One-to-N mapping. + * A custom record-to-bean mapping function for findMulti. + * Opposite of Finder::map(). Maps child beans to parents. + * + * Usage: + * + * + * $collection = R::findMulti( 'shop,product', + * 'SELECT shop.*, product.* FROM shop + * LEFT JOIN product ON product.shop_id = shop.id', + * [], [ + * Finder::onmap( 'product', 'shop' ), + * ]); + * + * + * Can also be used for instance to attach related beans + * in one-go to save some queries: + * + * Given $users that have a country_id: + * + * + * $all = R::findMulti('country', + * R::genSlots( $users, + * 'SELECT country.* FROM country WHERE id IN ( %s )' ), + * array_column( $users, 'country_id' ), + * [Finder::onmap('country', $gebruikers)] + * ); + * + * + * For your convenience, an even shorter notation has been added: + * + * $countries = R::loadJoined( $users, 'country' ); + * + * @param string $parentName name of the parent bean + * @param string|array $childName name of the child bean + * + * @return array + */ + public static function onMap($parentName,$childNameOrBeans) { + return array( + 'a' => $parentName, + 'b' => $childNameOrBeans, + 'matcher' => array( $parentName, "{$parentName}_id" ), + 'do' => 'match' + ); + } + + /** + * Finds a bean using a type and a where clause (SQL). + * As with most Query tools in RedBean you can provide values to + * be inserted in the SQL statement by populating the value + * array parameter; you can either use the question mark notation + * or the slot-notation (:keyname). + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function find( $type, $sql = NULL, $bindings = array() ) + { + if ( !is_array( $bindings ) ) { + throw new RedException( + 'Expected array, ' . gettype( $bindings ) . ' given.' + ); + } + + return $this->redbean->find( $type, array(), $sql, $bindings ); + } + + /** + * Like find() but also exports the beans as an array. + * This method will perform a find-operation. For every bean + * in the result collection this method will call the export() method. + * This method returns an array containing the array representations + * of every bean in the result set. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function findAndExport( $type, $sql = NULL, $bindings = array() ) + { + $arr = array(); + foreach ( $this->find( $type, $sql, $bindings ) as $key => $item ) { + $arr[] = $item->export(); + } + + return $arr; + } + + /** + * Like find() but returns just one bean instead of an array of beans. + * This method will return only the first bean of the array. + * If no beans are found, this method will return NULL. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public function findOne( $type, $sql = NULL, $bindings = array() ) + { + $sql = $this->toolbox->getWriter()->glueLimitOne( $sql ); + + $items = $this->find( $type, $sql, $bindings ); + + if ( empty($items) ) { + return NULL; + } + + return reset( $items ); + } + + /** + * Like find() but returns the last bean of the result array. + * Opposite of Finder::findLast(). + * If no beans are found, this method will return NULL. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public function findLast( $type, $sql = NULL, $bindings = array() ) + { + $items = $this->find( $type, $sql, $bindings ); + + if ( empty($items) ) { + return NULL; + } + + return end( $items ); + } + + /** + * Tries to find beans of a certain type, + * if no beans are found, it dispenses a bean of that type. + * Note that this function always returns an array. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function findOrDispense( $type, $sql = NULL, $bindings = array() ) + { + $foundBeans = $this->find( $type, $sql, $bindings ); + + if ( empty( $foundBeans ) ) { + return array( $this->redbean->dispense( $type ) ); + } else { + return $foundBeans; + } + } + + /** + * Finds a BeanCollection using the repository. + * A bean collection can be used to retrieve one bean at a time using + * cursors - this is useful for processing large datasets. A bean collection + * will not load all beans into memory all at once, just one at a time. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return BeanCollection + */ + public function findCollection( $type, $sql, $bindings = array() ) + { + return $this->redbean->findCollection( $type, $sql, $bindings ); + } + + /** + * Finds or creates a bean. + * Tries to find a bean with certain properties specified in the second + * parameter ($like). If the bean is found, it will be returned. + * If multiple beans are found, only the first will be returned. + * If no beans match the criteria, a new bean will be dispensed, + * the criteria will be imported as properties and this new bean + * will be stored and returned. + * + * Format of criteria set: property => value + * The criteria set also supports OR-conditions: property => array( value1, orValue2 ) + * + * @param string $type type of bean to search for + * @param array $like criteria set describing bean to search for + * @param boolean $hasBeenCreated set to TRUE if bean has been created + * + * @return OODBBean + */ + public function findOrCreate( $type, $like = array(), $sql = '', &$hasBeenCreated = false ) + { + $sql = $this->toolbox->getWriter()->glueLimitOne( $sql ); + $beans = $this->findLike( $type, $like, $sql ); + if ( count( $beans ) ) { + $bean = reset( $beans ); + $hasBeenCreated = false; + return $bean; + } + + $bean = $this->redbean->dispense( $type ); + $bean->import( $like ); + $this->redbean->store( $bean ); + $hasBeenCreated = true; + return $bean; + } + + /** + * Finds beans by its type and a certain criteria set. + * + * Format of criteria set: property => value + * The criteria set also supports OR-conditions: property => array( value1, orValue2 ) + * + * If the additional SQL is a condition, this condition will be glued to the rest + * of the query using an AND operator. Note that this is as far as this method + * can go, there is no way to glue additional SQL using an OR-condition. + * This method provides access to an underlying mechanism in the RedBeanPHP architecture + * to find beans using criteria sets. However, please do not use this method + * for complex queries, use plain SQL instead ( the regular find method ) as it is + * more suitable for the job. This method is + * meant for basic search-by-example operations. + * + * @param string $type type of bean to search for + * @param array $conditions criteria set describing the bean to search for + * @param string $sql additional SQL (for sorting) + * @param array $bindings bindings + * + * @return array + */ + public function findLike( $type, $conditions = array(), $sql = '', $bindings = array() ) + { + return $this->redbean->find( $type, $conditions, $sql, $bindings ); + } + + /** + * Returns a hashmap with bean arrays keyed by type using an SQL + * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review' + * this method will return movie and review beans. + * + * Example: + * + * + * $stuff = $finder->findMulti('movie,review', ' + * SELECT movie.*, review.* FROM movie + * LEFT JOIN review ON review.movie_id = movie.id'); + * + * + * After this operation, $stuff will contain an entry 'movie' containing all + * movies and an entry named 'review' containing all reviews (all beans). + * You can also pass bindings. + * + * If you want to re-map your beans, so you can use $movie->ownReviewList without + * having RedBeanPHP executing an SQL query you can use the fourth parameter to + * define a selection of remapping closures. + * + * The remapping argument (optional) should contain an array of arrays. + * Each array in the remapping array should contain the following entries: + * + * + * array( + * 'a' => TYPE A + * 'b' => TYPE B OR BEANS + * 'matcher' => + * MATCHING FUNCTION ACCEPTING A, B and ALL BEANS + * OR ARRAY + * WITH FIELD on B that should match with FIELD on A + * AND FIELD on A that should match with FIELD on B + * OR TRUE + * TO JUST PERFORM THE DO-FUNCTION ON EVERY A-BEAN + * + * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS + * (ONLY IF MATCHER IS ALSO A FUNCTION) + * ) + * + * + * Using this mechanism you can build your own 'preloader' with tiny function + * snippets (and those can be re-used and shared online of course). + * + * Example: + * + * + * array( + * 'a' => 'movie' //define A as movie + * 'b' => 'review' //define B as review + * matcher' => function( $a, $b ) { + * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id + * } + * 'do' => function( $a, $b ) { + * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie + * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'. + * } + * ) + * + * + * The Query Template parameter is optional as well but can be used to + * set a different SQL template (sprintf-style) for processing the original query. + * + * @note the SQL query provided IS NOT THE ONE used internally by this function, + * this function will pre-process the query to get all the data required to find the beans. + * + * @note if you use the 'book.*' notation make SURE you're + * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because + * it's actually an SQL-like template SLOT, not real SQL. + * + * @note instead of an SQL query you can pass a result array as well. + * + * @note the performance of this function is poor, if you deal with large number of records + * please use plain SQL instead. This function has been added as a bridge between plain SQL + * and bean oriented approaches but it is really on the edge of both worlds. You can safely + * use this function to load additional records as beans in paginated context, let's say + * 50-250 records. Anything above that will gradually perform worse. RedBeanPHP was never + * intended to replace SQL but offer tooling to integrate SQL with object oriented + * designs. If you have come to this function, you have reached the final border between + * SQL-oriented design and OOP. Anything after this will be just as good as custom mapping + * or plain old database querying. I recommend the latter. + * + * @param string|array $types a list of types (either array or comma separated string) + * @param string|array $sql optional, an SQL query or an array of prefetched records + * @param array $bindings optional, bindings for SQL query + * @param array $remappings optional, an array of remapping arrays + * @param string $queryTemplate optional, query template + * + * @return array + */ + public function findMulti( $types, $sql = NULL, $bindings = array(), $remappings = array(), $queryTemplate = ' %s.%s AS %s__%s' ) + { + if ( !is_array( $types ) ) $types = array_map( 'trim', explode( ',', $types ) ); + if ( is_null( $sql ) ) { + $beans = array(); + foreach( $types as $type ) $beans[$type] = $this->redbean->find( $type ); + } else { + if ( !is_array( $sql ) ) { + $writer = $this->toolbox->getWriter(); + $adapter = $this->toolbox->getDatabaseAdapter(); + + //Repair the query, replace book.* with book.id AS book_id etc.. + foreach( $types as $type ) { + $regex = "#( (`?{$type}`?)\.\*)#"; + if ( preg_match( $regex, $sql, $matches ) ) { + $pattern = $matches[1]; + $table = $matches[2]; + $newSelectorArray = array(); + $columns = $writer->getColumns( $type ); + foreach( $columns as $column => $definition ) { + $newSelectorArray[] = sprintf( $queryTemplate, $table, $column, $type, $column ); + } + $newSelector = implode( ',', $newSelectorArray ); + $sql = str_replace( $pattern, $newSelector, $sql ); + } + } + + $rows = $adapter->get( $sql, $bindings ); + } else { + $rows = $sql; + } + + //Gather the bean data from the query results using the prefix + $wannaBeans = array(); + foreach( $types as $type ) { + $wannaBeans[$type] = array(); + $prefix = "{$type}__"; + foreach( $rows as $rowkey=>$row ) { + $wannaBean = array(); + foreach( $row as $cell => $value ) { + if ( strpos( $cell, $prefix ) === 0 ) { + $property = substr( $cell, strlen( $prefix ) ); + unset( $rows[$rowkey][$cell] ); + $wannaBean[$property] = $value; + } + } + if ( !isset( $wannaBean['id'] ) ) continue; + if ( is_null( $wannaBean['id'] ) ) continue; + $wannaBeans[$type][$wannaBean['id']] = $wannaBean; + } + } + + //Turn the rows into beans + $beans = array(); + foreach( $wannaBeans as $type => $wannabees ) { + $beans[$type] = $this->redbean->convertToBeans( $type, $wannabees ); + } + } + + //Apply additional re-mappings + foreach($remappings as $remapping) { + $a = $remapping['a']; + $b = $remapping['b']; + if (is_array($b)) { + $firstBean = reset($b); + $type = $firstBean->getMeta('type'); + $beans[$type] = $b; + $b = $type; + } + $matcher = $remapping['matcher']; + if (is_callable($matcher) || $matcher === TRUE) { + $do = $remapping['do']; + foreach( $beans[$a] as $bean ) { + if ( $matcher === TRUE ) { + $do( $bean, $beans[$b], $beans, $remapping ); + continue; + } + foreach( $beans[$b] as $putBean ) { + if ( $matcher( $bean, $putBean, $beans ) ) $do( $bean, $putBean, $beans, $remapping ); + } + } + } else { + list($field1, $field2) = $matcher; + foreach( $beans[$b] as $key => $bean ) { + $beans[$b][$key]->{$field1} = $beans[$a][$bean->{$field2}]; + } + } + } + return $beans; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\RedException\SQL as SQLException; + +/** + * Association Manager. + * The association manager can be used to create and manage + * many-to-many relations (for example sharedLists). In a many-to-many relation, + * one bean can be associated with many other beans, while each of those beans + * can also be related to multiple beans. + * + * @file RedBeanPHP/AssociationManager.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class AssociationManager extends Observable +{ + /** + * @var OODB + */ + protected $oodb; + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * @var QueryWriter + */ + protected $writer; + + /** + * Exception handler. + * Fluid and Frozen mode have different ways of handling + * exceptions. Fluid mode (using the fluid repository) ignores + * exceptions caused by the following: + * + * - missing tables + * - missing column + * + * In these situations, the repository will behave as if + * no beans could be found. This is because in fluid mode + * it might happen to query a table or column that has not been + * created yet. In frozen mode, this is not supposed to happen + * and the corresponding exceptions will be thrown. + * + * @param \Exception $exception exception + * + * @return void + */ + private function handleException( \Exception $exception ) + { + if ( $this->oodb->isFrozen() || !$this->writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), + $exception->getDriverDetails() + ) + ) { + throw $exception; + } + } + + /** + * Internal method. + * Returns the many-to-many related rows of table $type for bean $bean using additional SQL in $sql and + * $bindings bindings. If $getLinks is TRUE, link rows are returned instead. + * + * @param OODBBean $bean reference bean instance + * @param string $type target bean type + * @param string $sql additional SQL snippet + * @param array $bindings bindings for query + * + * @return array + */ + private function relatedRows( $bean, $type, $sql = '', $bindings = array() ) + { + $ids = array( $bean->id ); + $sourceType = $bean->getMeta( 'type' ); + try { + return $this->writer->queryRecordRelated( $sourceType, $type, $ids, $sql, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + return array(); + } + } + + /** + * Associates a pair of beans. This method associates two beans, no matter + * what types. Accepts a base bean that contains data for the linking record. + * This method is used by associate. This method also accepts a base bean to be used + * as the template for the link record in the database. + * + * @param OODBBean $bean1 first bean + * @param OODBBean $bean2 second bean + * @param OODBBean $bean base bean (association record) + * + * @return mixed + */ + protected function associateBeans( OODBBean $bean1, OODBBean $bean2, OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + $property1 = $bean1->getMeta( 'type' ) . '_id'; + $property2 = $bean2->getMeta( 'type' ) . '_id'; + + if ( $property1 == $property2 ) { + $property2 = $bean2->getMeta( 'type' ) . '2_id'; + } + + $this->oodb->store( $bean1 ); + $this->oodb->store( $bean2 ); + + $bean->setMeta( "cast.$property1", "id" ); + $bean->setMeta( "cast.$property2", "id" ); + $bean->setMeta( 'sys.buildcommand.unique', array( $property1, $property2 ) ); + + $bean->$property1 = $bean1->id; + $bean->$property2 = $bean2->id; + + $results = array(); + + try { + $id = $this->oodb->store( $bean ); + $results[] = $id; + } catch ( SQLException $exception ) { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), + array( QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION ), + $exception->getDriverDetails() ) + ) { + throw $exception; + } + } + + return $results; + } + + /** + * Constructor, creates a new instance of the Association Manager. + * The association manager can be used to create and manage + * many-to-many relations (for example sharedLists). In a many-to-many relation, + * one bean can be associated with many other beans, while each of those beans + * can also be related to multiple beans. To create an Association Manager + * instance you'll need to pass a ToolBox object. + * + * @param ToolBox $tools toolbox supplying core RedBeanPHP objects + */ + public function __construct( ToolBox $tools ) + { + $this->oodb = $tools->getRedBean(); + $this->adapter = $tools->getDatabaseAdapter(); + $this->writer = $tools->getWriter(); + $this->toolbox = $tools; + } + + /** + * Creates a table name based on a types array. + * Manages the get the correct name for the linking table for the + * types provided. + * + * @param array $types 2 types as strings + * + * @return string + */ + public function getTable( $types ) + { + return $this->writer->getAssocTable( $types ); + } + + /** + * Associates two beans in a many-to-many relation. + * This method will associate two beans and store the connection between the + * two in a link table. Instead of two single beans this method also accepts + * two sets of beans. Returns the ID or the IDs of the linking beans. + * + * @param OODBBean|array $beans1 one or more beans to form the association + * @param OODBBean|array $beans2 one or more beans to form the association + * + * @return array + */ + public function associate( $beans1, $beans2 ) + { + if ( !is_array( $beans1 ) ) { + $beans1 = array( $beans1 ); + } + + if ( !is_array( $beans2 ) ) { + $beans2 = array( $beans2 ); + } + + $results = array(); + foreach ( $beans1 as $bean1 ) { + foreach ( $beans2 as $bean2 ) { + $table = $this->getTable( array( $bean1->getMeta( 'type' ), $bean2->getMeta( 'type' ) ) ); + $bean = $this->oodb->dispense( $table ); + $results[] = $this->associateBeans( $bean1, $bean2, $bean ); + } + } + + return ( count( $results ) > 1 ) ? $results : reset( $results ); + } + + /** + * Counts the number of related beans in an N-M relation. + * This method returns the number of beans of type $type associated + * with reference bean(s) $bean. The query can be tuned using an + * SQL snippet for additional filtering. + * + * @param OODBBean|array $bean a bean object or an array of beans + * @param string $type type of bean you're interested in + * @param string $sql SQL snippet (optional) + * @param array $bindings bindings for your SQL string + * + * @return integer + */ + public function relatedCount( $bean, $type, $sql = NULL, $bindings = array() ) + { + if ( !( $bean instanceof OODBBean ) ) { + throw new RedException( + 'Expected array or OODBBean but got:' . gettype( $bean ) + ); + } + + if ( !$bean->id ) { + return 0; + } + + $beanType = $bean->getMeta( 'type' ); + + try { + return $this->writer->queryRecordCountRelated( $beanType, $type, $bean->id, $sql, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + + return 0; + } + } + + /** + * Breaks the association between two beans. This method unassociates two beans. If the + * method succeeds the beans will no longer form an association. In the database + * this means that the association record will be removed. This method uses the + * OODB trash() method to remove the association links, thus giving FUSE models the + * opportunity to hook-in additional business logic. If the $fast parameter is + * set to boolean TRUE this method will remove the beans without their consent, + * bypassing FUSE. This can be used to improve performance. + * + * @param OODBBean $beans1 first bean in target association + * @param OODBBean $beans2 second bean in target association + * @param boolean $fast if TRUE, removes the entries by query without FUSE + * + * @return void + */ + public function unassociate( $beans1, $beans2, $fast = NULL ) + { + $beans1 = ( !is_array( $beans1 ) ) ? array( $beans1 ) : $beans1; + $beans2 = ( !is_array( $beans2 ) ) ? array( $beans2 ) : $beans2; + + foreach ( $beans1 as $bean1 ) { + foreach ( $beans2 as $bean2 ) { + try { + $this->oodb->store( $bean1 ); + $this->oodb->store( $bean2 ); + + $type1 = $bean1->getMeta( 'type' ); + $type2 = $bean2->getMeta( 'type' ); + + $row = $this->writer->queryRecordLink( $type1, $type2, $bean1->id, $bean2->id ); + + if ( !$row ) return; + + $linkType = $this->getTable( array( $type1, $type2 ) ); + + if ( $fast ) { + $this->writer->deleteRecord( $linkType, array( 'id' => $row['id'] ) ); + + return; + } + + $beans = $this->oodb->convertToBeans( $linkType, array( $row ) ); + + if ( count( $beans ) > 0 ) { + $bean = reset( $beans ); + $this->oodb->trash( $bean ); + } + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + } + } + } + + /** + * Removes all relations for a bean. This method breaks every connection between + * a certain bean $bean and every other bean of type $type. Warning: this method + * is really fast because it uses a direct SQL query however it does not inform the + * models about this. If you want to notify FUSE models about deletion use a foreach-loop + * with unassociate() instead. (that might be slower though) + * + * @param OODBBean $bean reference bean + * @param string $type type of beans that need to be unassociated + * + * @return void + */ + public function clearRelations( OODBBean $bean, $type ) + { + $this->oodb->store( $bean ); + try { + $this->writer->deleteRelations( $bean->getMeta( 'type' ), $type, $bean->id ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + } + + /** + * Returns all the beans associated with $bean. + * This method will return an array containing all the beans that have + * been associated once with the associate() function and are still + * associated with the bean specified. The type parameter indicates the + * type of beans you are looking for. You can also pass some extra SQL and + * values for that SQL to filter your results after fetching the + * related beans. + * + * Don't try to make use of subqueries, a subquery using IN() seems to + * be slower than two queries! + * + * Since 3.2, you can now also pass an array of beans instead just one + * bean as the first parameter. + * + * @param OODBBean|array $bean the bean you have + * @param string $type the type of beans you want + * @param string $sql SQL snippet for extra filtering + * @param array $bindings values to be inserted in SQL slots + * + * @return array + */ + public function related( $bean, $type, $sql = '', $bindings = array() ) + { + $sql = $this->writer->glueSQLCondition( $sql ); + $rows = $this->relatedRows( $bean, $type, $sql, $bindings ); + $links = array(); + + foreach ( $rows as $key => $row ) { + if ( !isset( $links[$row['id']] ) ) $links[$row['id']] = array(); + $links[$row['id']][] = $row['linked_by']; + unset( $rows[$key]['linked_by'] ); + } + + $beans = $this->oodb->convertToBeans( $type, $rows ); + foreach ( $beans as $bean ) $bean->setMeta( 'sys.belongs-to', $links[$bean->id] ); + + return $beans; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\OODBBean as OODBBean; + +/** + * Bean Helper Interface. + * + * Interface for Bean Helper. + * A little bolt that glues the whole machinery together. + * The Bean Helper is passed to the OODB RedBeanPHP Object to + * faciliatte the creation of beans and providing them with + * a toolbox. The Helper also facilitates the FUSE feature, + * determining how beans relate to their models. By overriding + * the getModelForBean method you can tune the FUSEing to + * fit your business application needs. + * + * @file RedBeanPHP/IBeanHelper.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface BeanHelper +{ + /** + * Returns a toolbox to empower the bean. + * This allows beans to perform OODB operations by themselves, + * as such the bean is a proxy for OODB. This allows beans to implement + * their magic getters and setters and return lists. + * + * @return ToolBox + */ + public function getToolbox(); + + /** + * Does approximately the same as getToolbox but also extracts the + * toolbox for you. + * This method returns a list with all toolbox items in Toolbox Constructor order: + * OODB, adapter, writer and finally the toolbox itself!. + * + * @return array + */ + public function getExtractedToolbox(); + + /** + * Given a certain bean this method will + * return the corresponding model. + * + * @param OODBBean $bean bean to obtain the corresponding model of + * + * @return SimpleModel|CustomModel|NULL + */ + public function getModelForBean( OODBBean $bean ); +} +} + +namespace RedBeanPHP\BeanHelper { + +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\Facade as Facade; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\SimpleModelHelper as SimpleModelHelper; + +/** + * Bean Helper. + * + * The Bean helper helps beans to access access the toolbox and + * FUSE models. This Bean Helper makes use of the facade to obtain a + * reference to the toolbox. + * + * @file RedBeanPHP/BeanHelperFacade.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SimpleFacadeBeanHelper implements BeanHelper +{ + /** + * Factory function to create instance of Simple Model, if any. + * + * @var \Closure + */ + private static $factory = null; + + /** + * Factory method using a customizable factory function to create + * the instance of the Simple Model. + * + * @param string $modelClassName name of the class + * + * @return SimpleModel + */ + public static function factory( $modelClassName ) + { + $factory = self::$factory; + return ( $factory ) ? $factory( $modelClassName ) : new $modelClassName(); + } + + /** + * Sets the factory function to create the model when using FUSE + * to connect a bean to a model. + * + * @param \Closure $factory factory function + * + * @return void + */ + public static function setFactoryFunction( $factory ) + { + self::$factory = $factory; + } + + /** + * @see BeanHelper::getToolbox + */ + public function getToolbox() + { + return Facade::getToolBox(); + } + + /** + * @see BeanHelper::getModelForBean + */ + public function getModelForBean( OODBBean $bean ) + { + $model = $bean->getMeta( 'type' ); + $prefix = defined( 'REDBEAN_MODEL_PREFIX' ) ? REDBEAN_MODEL_PREFIX : '\\Model_'; + + if ( strpos( $model, '_' ) !== FALSE ) { + $modelParts = explode( '_', $model ); + $modelName = ''; + foreach( $modelParts as $part ) { + $modelName .= ucfirst( $part ); + } + $modelName = $prefix . $modelName; + if ( !class_exists( $modelName ) ) { + $modelName = $prefix . ucfirst( $model ); + if ( !class_exists( $modelName ) ) { + return NULL; + } + } + } else { + $modelName = $prefix . ucfirst( $model ); + if ( !class_exists( $modelName ) ) { + return NULL; + } + } + $obj = self::factory( $modelName ); + $obj->loadBean( $bean ); + return $obj; + } + + /** + * @see BeanHelper::getExtractedToolbox + */ + public function getExtractedToolbox() + { + return Facade::getExtractedToolbox(); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\OODBBean as OODBBean; + +/** + * SimpleModel + * Base Model For All RedBeanPHP Models using FUSE. + * + * RedBeanPHP FUSE is a mechanism to connect beans to posthoc + * models. Models are connected to beans by naming conventions. + * Actions on beans will result in actions on models. + * + * @file RedBeanPHP/SimpleModel.php + * @author Gabor de Mooij and the RedBeanPHP Team + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SimpleModel +{ + /** + * @var OODBBean + */ + protected $bean; + + /** + * Used by FUSE: the ModelHelper class to connect a bean to a model. + * This method loads a bean in the model. + * + * @param OODBBean $bean bean to load + * + * @return void + */ + public function loadBean( OODBBean $bean ) + { + $this->bean = $bean; + } + + /** + * Magic Getter to make the bean properties available from + * the $this-scope. + * + * @note this method returns a value, not a reference! + * To obtain a reference unbox the bean first! + * + * @param string $prop property to get + * + * @return mixed + */ + public function __get( $prop ) + { + return $this->bean->$prop; + } + + /** + * Magic Setter. + * Sets the value directly as a bean property. + * + * @param string $prop property to set value of + * @param mixed $value value to set + * + * @return void + */ + public function __set( $prop, $value ) + { + $this->bean->$prop = $value; + } + + /** + * Isset implementation. + * Implements the isset function for array-like access. + * + * @param string $key key to check + * + * @return boolean + */ + public function __isset( $key ) + { + return isset( $this->bean->$key ); + } + + /** + * Box the bean using the current model. + * This method wraps the current bean in this model. + * This method can be reached using FUSE through a simple + * OODBBean. The method returns a RedBeanPHP Simple Model. + * This is useful if you would like to rely on PHP type hinting. + * You can box your beans before passing them to functions or methods + * with typed parameters. + * + * Note about beans vs models: + * Use unbox to obtain the bean powering the model. If you want to use bean functionality, + * you should -always- unbox first. While some functionality (like magic get/set) is + * available in the model, this is just read-only. To use a model as a typical RedBean + * OODBBean you should always unbox the model to a bean. Models are meant to + * expose only domain logic added by the developer (business logic, no ORM logic). + * + * @return SimpleModel + */ + public function box() + { + return $this; + } + + /** + * Unbox the bean from the model. + * This method returns the bean inside the model. + * + * Note about beans vs models: + * Use unbox to obtain the bean powering the model. If you want to use bean functionality, + * you should -always- unbox first. While some functionality (like magic get/set) is + * available in the model, this is just read-only. To use a model as a typical RedBean + * OODBBean you should always unbox the model to a bean. Models are meant to + * expose only domain logic added by the developer (business logic, no ORM logic). + * + * @return OODBBean + */ + public function unbox() + { + return $this->bean; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Observer as Observer; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\Observable as Observable; + +/** + * RedBean Model Helper. + * + * Connects beans to models. + * This is the core of so-called FUSE. + * + * @file RedBeanPHP/ModelHelper.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SimpleModelHelper implements Observer +{ + /** + * Gets notified by an observable. + * This method decouples the FUSE system from the actual beans. + * If a FUSE event happens 'update', this method will attempt to + * invoke the corresponding method on the bean. + * + * @param string $eventName i.e. 'delete', 'after_delete' + * @param OODBean $bean affected bean + * + * @return void + */ + public function onEvent( $eventName, $bean ) + { + $bean->$eventName(); + } + + /** + * Attaches the FUSE event listeners. Now the Model Helper will listen for + * CRUD events. If a CRUD event occurs it will send a signal to the model + * that belongs to the CRUD bean and this model will take over control from + * there. This method will attach the following event listeners to the observable: + * + * - 'update' (gets called by R::store, before the records gets inserted / updated) + * - 'after_update' (gets called by R::store, after the records have been inserted / updated) + * - 'open' (gets called by R::load, after the record has been retrieved) + * - 'delete' (gets called by R::trash, before deletion of record) + * - 'after_delete' (gets called by R::trash, after deletion) + * - 'dispense' (gets called by R::dispense) + * + * For every event type, this method will register this helper as a listener. + * The observable will notify the listener (this object) with the event ID and the + * affected bean. This helper will then process the event (onEvent) by invoking + * the event on the bean. If a bean offers a method with the same name as the + * event ID, this method will be invoked. + * + * @param Observable $observable object to observe + * + * @return void + */ + public function attachEventListeners( Observable $observable ) + { + foreach ( array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ) as $eventID ) { + $observable->addEventListener( $eventID, $this ); + } + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\AssociationManager as AssociationManager; +use RedBeanPHP\OODBBean as OODBBean; + +/** + * RedBeanPHP Tag Manager. + * + * The tag manager offers an easy way to quickly implement basic tagging + * functionality. + * + * Provides methods to tag beans and perform tag-based searches in the + * bean database. + * + * @file RedBeanPHP/TagManager.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class TagManager +{ + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * @var AssociationManager + */ + protected $associationManager; + + /** + * @var OODBBean + */ + protected $redbean; + + /** + * Checks if the argument is a comma separated string, in this case + * it will split the string into words and return an array instead. + * In case of an array the argument will be returned 'as is'. + * + * @param array|string $tagList list of tags + * + * @return array + */ + private function extractTagsIfNeeded( $tagList ) + { + if ( $tagList !== FALSE && !is_array( $tagList ) ) { + $tags = explode( ',', (string) $tagList ); + } else { + $tags = $tagList; + } + + return $tags; + } + + /** + * Finds a tag bean by it's title. + * Internal method. + * + * @param string $title title to search for + * + * @return OODBBean + */ + protected function findTagByTitle( $title ) + { + $beans = $this->redbean->find( 'tag', array( 'title' => array( $title ) ) ); + + if ( $beans ) { + $bean = reset( $beans ); + + return $bean; + } + + return NULL; + } + + /** + * Constructor. + * The tag manager offers an easy way to quickly implement basic tagging + * functionality. + * + * @param ToolBox $toolbox toolbox object + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + + $this->associationManager = $this->redbean->getAssociationManager(); + } + + /** + * Tests whether a bean has been associated with one ore more + * of the listed tags. If the third parameter is TRUE this method + * will return TRUE only if all tags that have been specified are indeed + * associated with the given bean, otherwise FALSE. + * If the third parameter is FALSE this + * method will return TRUE if one of the tags matches, FALSE if none + * match. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::hasTag( $blog, 'horror,movie', TRUE ); + * + * + * The example above returns TRUE if the $blog bean has been tagged + * as BOTH horror and movie. If the post has only been tagged as 'movie' + * or 'horror' this operation will return FALSE because the third parameter + * has been set to TRUE. + * + * @param OODBBean $bean bean to check for tags + * @param array|string $tags list of tags + * @param boolean $all whether they must all match or just some + * + * @return boolean + */ + public function hasTag( $bean, $tags, $all = FALSE ) + { + $foundtags = $this->tag( $bean ); + + $tags = $this->extractTagsIfNeeded( $tags ); + $same = array_intersect( $tags, $foundtags ); + + if ( $all ) { + return ( implode( ',', $same ) === implode( ',', $tags ) ); + } + + return (bool) ( count( $same ) > 0 ); + } + + /** + * Removes all specified tags from the bean. The tags specified in + * the second parameter will no longer be associated with the bean. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::untag( $blog, 'smart,interesting' ); + * + * + * In the example above, the $blog bean will no longer + * be associated with the tags 'smart' and 'interesting'. + * + * @param OODBBean $bean tagged bean + * @param array $tagList list of tags (names) + * + * @return void + */ + public function untag( $bean, $tagList ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + + foreach ( $tags as $tag ) { + if ( $t = $this->findTagByTitle( $tag ) ) { + $this->associationManager->unassociate( $bean, $t ); + } + } + } + + /** + * Part of RedBeanPHP Tagging API. + * Tags a bean or returns tags associated with a bean. + * If $tagList is NULL or omitted this method will return a + * comma separated list of tags associated with the bean provided. + * If $tagList is a comma separated list (string) of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::tag( $meal, "TexMex,Mexican" ); + * $tags = R::tag( $meal ); + * + * + * The first line in the example above will tag the $meal + * as 'TexMex' and 'Mexican Cuisine'. The second line will + * retrieve all tags attached to the meal object. + * + * @param OODBBean $bean bean to tag + * @param mixed $tagList tags to attach to the specified bean + * + * @return string + */ + public function tag( OODBBean $bean, $tagList = NULL ) + { + if ( is_null( $tagList ) ) { + + $tags = $bean->sharedTag; + $foundTags = array(); + + foreach ( $tags as $tag ) { + $foundTags[] = $tag->title; + } + + return $foundTags; + } + + $this->associationManager->clearRelations( $bean, 'tag' ); + $this->addTags( $bean, $tagList ); + + return $tagList; + } + + /** + * Part of RedBeanPHP Tagging API. + * Adds tags to a bean. + * If $tagList is a comma separated list of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::addTags( $blog, ["halloween"] ); + * + * + * The example adds the tag 'halloween' to the $blog + * bean. + * + * @param OODBBean $bean bean to tag + * @param array $tagList list of tags to add to bean + * + * @return void + */ + public function addTags( OODBBean $bean, $tagList ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + + if ( $tagList === FALSE ) { + return; + } + + foreach ( $tags as $tag ) { + if ( !$t = $this->findTagByTitle( $tag ) ) { + $t = $this->redbean->dispense( 'tag' ); + $t->title = $tag; + + $this->redbean->store( $t ); + } + + $this->associationManager->associate( $bean, $t ); + } + } + + /** + * Returns all beans that have been tagged with one or more + * of the specified tags. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::tagged( + * 'movie', + * 'horror,gothic', + * ' ORDER BY movie.title DESC LIMIT ?', + * [ 10 ] + * ); + * + * + * The example uses R::tagged() to find all movies that have been + * tagged as 'horror' or 'gothic', order them by title and limit + * the number of movies to be returned to 10. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional SQL (use only for pagination) + * @param array $bindings bindings + * + * @return array + */ + public function tagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + $records = $this->toolbox->getWriter()->queryTagged( $beanType, $tags, FALSE, $sql, $bindings ); + + return $this->redbean->convertToBeans( $beanType, $records ); + } + + /** + * Returns all beans that have been tagged with ALL of the tags given. + * This method works the same as R::tagged() except that this method only returns + * beans that have been tagged with all the specified labels. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::taggedAll( + * 'movie', + * [ 'gothic', 'short' ], + * ' ORDER BY movie.id DESC LIMIT ? ', + * [ 4 ] + * ); + * + * + * The example above returns at most 4 movies (due to the LIMIT clause in the SQL + * Query Snippet) that have been tagged as BOTH 'short' AND 'gothic'. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return array + */ + public function taggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + $records = $this->toolbox->getWriter()->queryTagged( $beanType, $tags, TRUE, $sql, $bindings ); + + return $this->redbean->convertToBeans( $beanType, $records ); + } + + /** + * Like taggedAll() but only counts. + * + * @see taggedAll + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public function countTaggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + return $this->toolbox->getWriter()->queryCountTagged( $beanType, $tags, TRUE, $sql, $bindings ); + } + + /** + * Like tagged() but only counts. + * + * @see tagged + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public function countTagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + return $this->toolbox->getWriter()->queryCountTagged( $beanType, $tags, FALSE, $sql, $bindings ); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\OODBBean as OODBBean; + +/** + * Label Maker. + * Makes so-called label beans. + * A label is a bean with only an id, type and name property. + * Labels can be used to create simple entities like categories, tags or enums. + * This service class provides convenience methods to deal with this kind of + * beans. + * + * @file RedBeanPHP/LabelMaker.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class LabelMaker +{ + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * Constructor. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * A label is a bean with only an id, type and name property. + * This function will dispense beans for all entries in the array. The + * values of the array will be assigned to the name property of each + * individual bean. + * + * + * $people = R::dispenseLabels( 'person', [ 'Santa', 'Claus' ] ); + * + * + * @param string $type type of beans you would like to have + * @param array $labels list of labels, names for each bean + * + * @return array + */ + public function dispenseLabels( $type, $labels ) + { + $labelBeans = array(); + foreach ( $labels as $label ) { + $labelBean = $this->toolbox->getRedBean()->dispense( $type ); + $labelBean->name = $label; + $labelBeans[] = $labelBean; + } + + return $labelBeans; + } + + /** + * Gathers labels from beans. This function loops through the beans, + * collects the value of the name property for each individual bean + * and stores the names in a new array. The array then gets sorted using the + * default sort function of PHP (sort). + * + * Usage: + * + * + * $o1->name = 'hamburger'; + * $o2->name = 'pizza'; + * implode( ',', R::gatherLabels( [ $o1, $o2 ] ) ); //hamburger,pizza + * + * + * Note that the return value is an array of strings, not beans. + * + * @param array $beans list of beans to loop through + * + * @return array + */ + public function gatherLabels( $beans ) + { + $labels = array(); + + foreach ( $beans as $bean ) { + $labels[] = $bean->name; + } + + sort( $labels ); + + return $labels; + } + + /** + * Fetches an ENUM from the database and creates it if necessary. + * An ENUM has the following format: + * + * + * ENUM:VALUE + * + * + * If you pass 'ENUM' only, this method will return an array of its + * values: + * + * + * implode( ',', R::gatherLabels( R::enum( 'flavour' ) ) ) //'BANANA,MOCCA' + * + * + * If you pass 'ENUM:VALUE' this method will return the specified enum bean + * and create it in the database if it does not exist yet: + * + * + * $bananaFlavour = R::enum( 'flavour:banana' ); + * $bananaFlavour->name; + * + * + * So you can use this method to set an ENUM value in a bean: + * + * + * $shake->flavour = R::enum( 'flavour:banana' ); + * + * + * the property flavour now contains the enum bean, a parent bean. + * In the database, flavour_id will point to the flavour record with name 'banana'. + * + * @param string $enum ENUM specification for label + * + * @return array|OODBBean + */ + public function enum( $enum ) + { + $oodb = $this->toolbox->getRedBean(); + + if ( strpos( $enum, ':' ) === FALSE ) { + $type = $enum; + $value = FALSE; + } else { + list( $type, $value ) = explode( ':', $enum ); + $value = preg_replace( '/\W+/', '_', strtoupper( trim( $value ) ) ); + } + + /** + * We use simply find here, we could use inspect() in fluid mode etc, + * but this would be useless. At first sight it looks clean, you could even + * bake this into find(), however, find not only has to deal with the primary + * search type, people can also include references in the SQL part, so avoiding + * find failures does not matter, this is still the quickest way making use + * of existing functionality. + * + * @note There seems to be a bug in XDebug v2.3.2 causing suppressed + * exceptions like these to surface anyway, to prevent this use: + * + * "xdebug.default_enable = 0" + * + * Also see Github Issue #464 + */ + $values = $oodb->find( $type ); + + if ( $value === FALSE ) { + return $values; + } + + foreach( $values as $enumItem ) { + if ( $enumItem->name === $value ) return $enumItem; + } + + $newEnumItems = $this->dispenseLabels( $type, array( $value ) ); + $newEnumItem = reset( $newEnumItems ); + + $oodb->store( $newEnumItem ); + + return $newEnumItem; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\RedException\SQL as SQLException; +use RedBeanPHP\Logger as Logger; +use RedBeanPHP\Logger\RDefault as RDefault; +use RedBeanPHP\Logger\RDefault\Debug as Debug; +use RedBeanPHP\Adapter as Adapter; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\BeanHelper\SimpleFacadeBeanHelper as SimpleFacadeBeanHelper; +use RedBeanPHP\Driver\RPDO as RPDO; +use RedBeanPHP\Util\MultiLoader as MultiLoader; +use RedBeanPHP\Util\Transaction as Transaction; +use RedBeanPHP\Util\Dump as Dump; +use RedBeanPHP\Util\DispenseHelper as DispenseHelper; +use RedBeanPHP\Util\ArrayTool as ArrayTool; +use RedBeanPHP\Util\QuickExport as QuickExport; +use RedBeanPHP\Util\MatchUp as MatchUp; +use RedBeanPHP\Util\Look as Look; +use RedBeanPHP\Util\Diff as Diff; +use RedBeanPHP\Util\Tree as Tree; +use RedBeanPHP\Util\Feature; + +/** + * RedBean Facade + * + * Version Information + * RedBean Version @version 5.6 + * + * This class hides the object landscape of + * RedBeanPHP behind a single letter class providing + * almost all functionality with simple static calls. + * + * @file RedBeanPHP/Facade.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Facade +{ + /** + * RedBeanPHP version constant. + */ + const C_REDBEANPHP_VERSION = '5.6'; + + /** + * @var ToolBox + */ + public static $toolbox; + + /** + * @var OODB + */ + private static $redbean; + + /** + * @var QueryWriter + */ + private static $writer; + + /** + * @var DBAdapter + */ + private static $adapter; + + /** + * @var AssociationManager + */ + private static $associationManager; + + /** + * @var TagManager + */ + private static $tagManager; + + /** + * @var DuplicationManager + */ + private static $duplicationManager; + + /** + * @var LabelMaker + */ + private static $labelMaker; + + /** + * @var Finder + */ + private static $finder; + + /** + * @var Tree + */ + private static $tree; + + /** + * @var Logger + */ + private static $logger; + + /** + * @var array + */ + private static $plugins = array(); + + /** + * @var string + */ + private static $exportCaseStyle = 'default'; + + /** + * @var flag allows transactions through facade in fluid mode + */ + private static $allowFluidTransactions = FALSE; + + /** + * @var flag allows to unfreeze if needed with store(all) + */ + private static $allowHybridMode = FALSE; + + /** + * Not in use (backward compatibility SQLHelper) + */ + public static $f; + + /** + * @var string + */ + public static $currentDB = ''; + + /** + * @var array + */ + public static $toolboxes = array(); + + /** + * Internal Query function, executes the desired query. Used by + * all facade query functions. This keeps things DRY. + * + * @param string $method desired query method (i.e. 'cell', 'col', 'exec' etc..) + * @param string $sql the sql you want to execute + * @param array $bindings array of values to be bound to query statement + * + * @return array + */ + private static function query( $method, $sql, $bindings ) + { + if ( !self::$redbean->isFrozen() ) { + try { + $rs = Facade::$adapter->$method( $sql, $bindings ); + } catch ( SQLException $exception ) { + if ( self::$writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ) + ,$exception->getDriverDetails() + ) + ) { + return ( $method === 'getCell' ) ? NULL : array(); + } else { + throw $exception; + } + } + + return $rs; + } else { + return Facade::$adapter->$method( $sql, $bindings ); + } + } + + /** + * Sets allow hybrid mode flag. In Hybrid mode (default off), + * store/storeAll take an extra argument to switch to fluid + * mode in case of an exception. You can use this to speed up + * fluid mode. This method returns the previous value of the + * flag. + * + * @param boolean $hybrid + */ + public static function setAllowHybridMode( $hybrid ) + { + $old = self::$allowHybridMode; + self::$allowHybridMode = $hybrid; + return $old; + } + + /** + * Returns the RedBeanPHP version string. + * The RedBeanPHP version string always has the same format "X.Y" + * where X is the major version number and Y is the minor version number. + * Point releases are not mentioned in the version string. + * + * @return string + */ + public static function getVersion() + { + return self::C_REDBEANPHP_VERSION; + } + + /** + * Returns the version string from the database server. + * + * @return string + */ + public static function getDatabaseServerVersion() + { + return self::$adapter->getDatabaseServerVersion(); + } + + /** + * Tests the database connection. + * Returns TRUE if connection has been established and + * FALSE otherwise. Suppresses any warnings that may + * occur during the testing process and catches all + * exceptions that might be thrown during the test. + * + * @return boolean + */ + public static function testConnection() + { + if ( !isset( self::$adapter ) ) return FALSE; + + $database = self::$adapter->getDatabase(); + try { + @$database->connect(); + } catch ( \Exception $e ) {} + return $database->isConnected(); + } + + /** + * Kickstarts redbean for you. This method should be called before you start using + * RedBeanPHP. The Setup() method can be called without any arguments, in this case it will + * try to create a SQLite database in /tmp called red.db (this only works on UNIX-like systems). + * + * Usage: + * + * + * R::setup( 'mysql:host=localhost;dbname=mydatabase', 'dba', 'dbapassword' ); + * + * + * You can replace 'mysql:' with the name of the database you want to use. + * Possible values are: + * + * - pgsql (PostgreSQL database) + * - sqlite (SQLite database) + * - mysql (MySQL database) + * - mysql (also for Maria database) + * - sqlsrv (MS SQL Server - community supported experimental driver) + * - CUBRID (CUBRID driver - basic support provided by Plugin) + * + * Note that setup() will not immediately establish a connection to the database. + * Instead, it will prepare the connection and connect 'lazily', i.e. the moment + * a connection is really required, for instance when attempting to load + * a bean. + * + * @param string $dsn Database connection string + * @param string $username Username for database + * @param string $password Password for database + * @param boolean $frozen TRUE if you want to setup in frozen mode + * @param boolean $partialBeans TRUE to enable partial bean updates + * @param array $options Additional (PDO) options to pass + * + * @return ToolBox + */ + public static function setup( $dsn = NULL, $username = NULL, $password = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( is_null( $dsn ) ) { + $dsn = 'sqlite:' . DIRECTORY_SEPARATOR . sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'red.db'; + } + + self::addDatabase( 'default', $dsn, $username, $password, $frozen, $partialBeans, $options ); + self::selectDatabase( 'default' ); + + return self::$toolbox; + } + + /** + * Toggles 'Narrow Field Mode'. + * In Narrow Field mode the queryRecord method will + * narrow its selection field to + * + * + * SELECT {table}.* + * + * + * instead of + * + * + * SELECT * + * + * + * This is a better way of querying because it allows + * more flexibility (for instance joins). However if you need + * the wide selector for backward compatibility; use this method + * to turn OFF Narrow Field Mode by passing FALSE. + * Default is TRUE. + * + * @param boolean $narrowField TRUE = Narrow Field FALSE = Wide Field + * + * @return void + */ + public static function setNarrowFieldMode( $mode ) + { + AQueryWriter::setNarrowFieldMode( $mode ); + } + + /** + * Toggles fluid transactions. By default fluid transactions + * are not active. Starting, committing or rolling back a transaction + * through the facade in fluid mode will have no effect. If you wish + * to replace this standard portable behavor with behavior depending + * on how the used database platform handles fluid (DDL) transactions + * set this flag to TRUE. + * + * @param boolean $mode allow fluid transaction mode + * + * @return void + */ + public static function setAllowFluidTransactions( $mode ) + { + self::$allowFluidTransactions = $mode; + } + + /** + * Toggles support for IS-NULL-conditions. + * If IS-NULL-conditions are enabled condition arrays + * for functions including findLike() are treated so that + * 'field' => NULL will be interpreted as field IS NULL + * instead of being skipped. Returns the previous + * value of the flag. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function useISNULLConditions( $mode ) + { + self::getWriter()->flushCache(); /* otherwise same queries might fail (see Unit test XNull) */ + return AQueryWriter::useISNULLConditions( $mode ); + } + + /** + * Wraps a transaction around a closure or string callback. + * If an Exception is thrown inside, the operation is automatically rolled back. + * If no Exception happens, it commits automatically. + * It also supports (simulated) nested transactions (that is useful when + * you have many methods that needs transactions but are unaware of + * each other). + * + * Example: + * + * + * $from = 1; + * $to = 2; + * $amount = 300; + * + * R::transaction(function() use($from, $to, $amount) + * { + * $accountFrom = R::load('account', $from); + * $accountTo = R::load('account', $to); + * $accountFrom->money -= $amount; + * $accountTo->money += $amount; + * R::store($accountFrom); + * R::store($accountTo); + * }); + * + * + * @param callable $callback Closure (or other callable) with the transaction logic + * + * @return mixed + */ + public static function transaction( $callback ) + { + return Transaction::transaction( self::$adapter, $callback ); + } + + /** + * Adds a database to the facade, afterwards you can select the database using + * selectDatabase($key), where $key is the name you assigned to this database. + * + * Usage: + * + * + * R::addDatabase( 'database-1', 'sqlite:/tmp/db1.txt' ); + * R::selectDatabase( 'database-1' ); //to select database again + * + * + * This method allows you to dynamically add (and select) new databases + * to the facade. Adding a database with the same key will cause an exception. + * + * @param string $key ID for the database + * @param string $dsn DSN for the database + * @param string $user user for connection + * @param NULL|string $pass password for connection + * @param bool $frozen whether this database is frozen or not + * + * @return void + */ + public static function addDatabase( $key, $dsn, $user = NULL, $pass = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( isset( self::$toolboxes[$key] ) ) { + throw new RedException( 'A database has already been specified for this key.' ); + } + + self::$toolboxes[$key] = self::createToolbox($dsn, $user, $pass, $frozen, $partialBeans, $options); + } + + /** + * Creates a toolbox. This method can be called if you want to use redbean non-static. + * It has the same interface as R::setup(). The createToolbx() method can be called + * without any arguments, in this case it will try to create a SQLite database in + * /tmp called red.db (this only works on UNIX-like systems). + * + * Usage: + * + * + * R::createToolbox( 'mysql:host=localhost;dbname=mydatabase', 'dba', 'dbapassword' ); + * + * + * You can replace 'mysql:' with the name of the database you want to use. + * Possible values are: + * + * - pgsql (PostgreSQL database) + * - sqlite (SQLite database) + * - mysql (MySQL database) + * - mysql (also for Maria database) + * - sqlsrv (MS SQL Server - community supported experimental driver) + * - CUBRID (CUBRID driver - basic support provided by Plugin) + * + * Note that createToolbox() will not immediately establish a connection to the database. + * Instead, it will prepare the connection and connect 'lazily', i.e. the moment + * a connection is really required, for instance when attempting to load a bean. + * + * @param string $dsn Database connection string + * @param string $username Username for database + * @param string $password Password for database + * @param boolean $frozen TRUE if you want to setup in frozen mode + * + * @return ToolBox + */ + public static function createToolbox( $dsn = NULL, $username = NULL, $password = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( is_object($dsn) ) { + $db = new RPDO( $dsn ); + $dbType = $db->getDatabaseType(); + } else { + $db = new RPDO( $dsn, $username, $password, $options ); + $dbType = substr( $dsn, 0, strpos( $dsn, ':' ) ); + } + + $adapter = new DBAdapter( $db ); + + $writers = array( + 'pgsql' => 'PostgreSQL', + 'sqlite' => 'SQLiteT', + 'cubrid' => 'CUBRID', + 'mysql' => 'MySQL', + 'sqlsrv' => 'SQLServer', + ); + + $wkey = trim( strtolower( $dbType ) ); + if ( !isset( $writers[$wkey] ) ) { + $wkey = preg_replace( '/\W/', '' , $wkey ); + throw new RedException( 'Unsupported database ('.$wkey.').' ); + } + $writerClass = '\\RedBeanPHP\\QueryWriter\\'.$writers[$wkey]; + $writer = new $writerClass( $adapter ); + $redbean = new OODB( $writer, $frozen ); + + if ( $partialBeans ) { + $redbean->getCurrentRepository()->usePartialBeans( $partialBeans ); + } + + return new ToolBox( $redbean, $adapter, $writer ); + } + + /** + * Determines whether a database identified with the specified key has + * already been added to the facade. This function will return TRUE + * if the database indicated by the key is available and FALSE otherwise. + * + * @param string $key the key/name of the database to check for + * + * @return boolean + */ + public static function hasDatabase( $key ) + { + return ( isset( self::$toolboxes[$key] ) ); + } + + /** + * Selects a different database for the Facade to work with. + * If you use the R::setup() you don't need this method. This method is meant + * for multiple database setups. This method selects the database identified by the + * database ID ($key). Use addDatabase() to add a new database, which in turn + * can be selected using selectDatabase(). If you use R::setup(), the resulting + * database will be stored under key 'default', to switch (back) to this database + * use R::selectDatabase( 'default' ). This method returns TRUE if the database has been + * switched and FALSE otherwise (for instance if you already using the specified database). + * + * @param string $key Key of the database to select + * + * @return boolean + */ + public static function selectDatabase( $key, $force = FALSE ) + { + if ( self::$currentDB === $key && !$force ) { + return FALSE; + } + + if ( !isset( self::$toolboxes[$key] ) ) { + throw new RedException( 'Database not found in registry. Add database using R::addDatabase().' ); + } + + self::configureFacadeWithToolbox( self::$toolboxes[$key] ); + self::$currentDB = $key; + + return TRUE; + } + + /** + * Toggles DEBUG mode. + * In Debug mode all SQL that happens under the hood will + * be printed to the screen and/or logged. + * If no database connection has been configured using R::setup() or + * R::selectDatabase() this method will throw an exception. + * + * There are 2 debug styles: + * + * Classic: separate parameter bindings, explicit and complete but less readable + * Fancy: interpersed bindings, truncates large strings, highlighted schema changes + * + * Fancy style is more readable but sometimes incomplete. + * + * The first parameter turns debugging ON or OFF. + * The second parameter indicates the mode of operation: + * + * 0 Log and write to STDOUT classic style (default) + * 1 Log only, class style + * 2 Log and write to STDOUT fancy style + * 3 Log only, fancy style + * + * This function always returns the logger instance created to generate the + * debug messages. + * + * @param boolean $tf debug mode (TRUE or FALSE) + * @param integer $mode mode of operation + * + * @return RDefault + * @throws RedException + */ + public static function debug( $tf = TRUE, $mode = 0 ) + { + if ($mode > 1) { + $mode -= 2; + $logger = new Debug; + } else { + $logger = new RDefault; + } + + if ( !isset( self::$adapter ) ) { + throw new RedException( 'Use R::setup() first.' ); + } + $logger->setMode($mode); + self::$adapter->getDatabase()->setDebugMode( $tf, $logger ); + + return $logger; + } + + /** + * Turns on the fancy debugger. + * In 'fancy' mode the debugger will output queries with bound + * parameters inside the SQL itself. This method has been added to + * offer a convenient way to activate the fancy debugger system + * in one call. + * + * @param boolean $toggle TRUE to activate debugger and select 'fancy' mode + * + * @return void + */ + public static function fancyDebug( $toggle = TRUE ) + { + self::debug( $toggle, 2 ); + } + + /** + * Inspects the database schema. If you pass the type of a bean this + * method will return the fields of its table in the database. + * The keys of this array will be the field names and the values will be + * the column types used to store their values. + * If no type is passed, this method returns a list of all tables in the database. + * + * @param string $type Type of bean (i.e. table) you want to inspect + * + * @return array + */ + public static function inspect( $type = NULL ) + { + return ($type === NULL) ? self::$writer->getTables() : self::$writer->getColumns( $type ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * If the second parameter is set to TRUE and + * Hybrid mode is allowed (default OFF for novice), then RedBeanPHP + * will automatically temporarily switch to fluid mode to attempt to store the + * bean in case of an SQLException. + * + * @param OODBBean|SimpleModel $bean bean to store + * @param boolean $unfreezeIfNeeded retries in fluid mode in hybrid mode + * + * @return integer|string + */ + public static function store( $bean, $unfreezeIfNeeded = FALSE ) + { + $result = NULL; + try { + $result = self::$redbean->store( $bean ); + } catch (SQLException $exception) { + $wasFrozen = self::$redbean->isFrozen(); + if ( !self::$allowHybridMode || !$unfreezeIfNeeded ) throw $exception; + self::freeze( FALSE ); + $result = self::$redbean->store( $bean ); + self::freeze( $wasFrozen ); + } + return $result; + } + + /** + * Toggles fluid or frozen mode. In fluid mode the database + * structure is adjusted to accomodate your objects. In frozen mode + * this is not the case. + * + * You can also pass an array containing a selection of frozen types. + * Let's call this chilly mode, it's just like fluid mode except that + * certain types (i.e. tables) aren't touched. + * + * @param boolean|array $tf mode of operation (TRUE means frozen) + */ + public static function freeze( $tf = TRUE ) + { + self::$redbean->freeze( $tf ); + } + + /** + * Loads multiple types of beans with the same ID. + * This might look like a strange method, however it can be useful + * for loading a one-to-one relation. In a typical 1-1 relation, + * you have two records sharing the same primary key. + * RedBeanPHP has only limited support for 1-1 relations. + * In general it is recommended to use 1-N for this. + * + * Usage: + * + * + * list( $author, $bio ) = R::loadMulti( 'author, bio', $id ); + * + * + * @param string|array $types the set of types to load at once + * @param mixed $id the common ID + * + * @return OODBBean + */ + public static function loadMulti( $types, $id ) + { + return MultiLoader::load( self::$redbean, $types, $id ); + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * @param string $snippet string to use after select (optional) + * + * @return OODBBean + */ + public static function load( $type, $id, $snippet = NULL ) + { + if ( $snippet !== NULL ) self::$writer->setSQLSelectSnippet( $snippet ); + $bean = self::$redbean->load( $type, $id ); + return $bean; + } + + /** + * Same as load, but selects the bean for update, thus locking the bean. + * This equals an SQL query like 'SELECT ... FROM ... FOR UPDATE'. + * Use this method if you want to load a bean you intend to UPDATE. + * This method should be used to 'LOCK a bean'. + * + * Usage: + * + * + * $bean = R::loadForUpdate( 'bean', $id ); + * ...update... + * R::store( $bean ); + * + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public static function loadForUpdate( $type, $id ) + { + return self::load( $type, $id, AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + } + + /** + * Same as find(), but selects the beans for update, thus locking the beans. + * This equals an SQL query like 'SELECT ... FROM ... FOR UPDATE'. + * Use this method if you want to load a bean you intend to UPDATE. + * This method should be used to 'LOCK a bean'. + * + * Usage: + * + * + * $bean = R::findForUpdate( + * 'bean', + * ' title LIKE ? ', + * array('title') + * ); + * ...update... + * R::store( $bean ); + * + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findForUpdate( $type, $sql = NULL, $bindings = array() ) + { + return self::find( $type, $sql, $bindings, AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + } + + /** + * Convenience method. + * Same as findForUpdate but returns just one bean and adds LIMIT-clause. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findOneForUpdate( $type, $sql = NULL, $bindings = array() ) + { + $sql = self::getWriter()->glueLimitOne( $sql ); + $beans = self::findForUpdate($type, $sql, $bindings); + return !empty($beans) ? reset($beans) : NULL; + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * This facade method also accepts a type-id combination, + * in the latter case this method will attempt to load the specified bean + * and THEN trash it. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * @param string|OODBBean|SimpleModel $beanOrType bean you want to remove from database + * @param integer $id ID if the bean to trash (optional, type-id variant only) + * + * @return void + */ + public static function trash( $beanOrType, $id = NULL ) + { + if ( is_string( $beanOrType ) ) return self::trash( self::load( $beanOrType, $id ) ); + return self::$redbean->trash( $beanOrType ); + } + + /** + * Dispenses a new RedBean OODB Bean for use with + * the rest of the methods. RedBeanPHP thinks in beans, the bean is the + * primary way to interact with RedBeanPHP and the database managed by + * RedBeanPHP. To load, store and delete data from the database using RedBeanPHP + * you exchange these RedBeanPHP OODB Beans. The only exception to this rule + * are the raw query methods like R::getCell() or R::exec() and so on. + * The dispense method is the 'preferred way' to create a new bean. + * + * Usage: + * + * + * $book = R::dispense( 'book' ); + * $book->title = 'My Book'; + * R::store( $book ); + * + * + * This method can also be used to create an entire bean graph at once. + * Given an array with keys specifying the property names of the beans + * and a special _type key to indicate the type of bean, one can + * make the Dispense Helper generate an entire hierarchy of beans, including + * lists. To make dispense() generate a list, simply add a key like: + * ownXList or sharedXList where X is the type of beans it contains and + * a set its value to an array filled with arrays representing the beans. + * Note that, although the type may have been hinted at in the list name, + * you still have to specify a _type key for every bean array in the list. + * Note that, if you specify an array to generate a bean graph, the number + * parameter will be ignored. + * + * Usage: + * + * + * $book = R::dispense( [ + * '_type' => 'book', + * 'title' => 'Gifted Programmers', + * 'author' => [ '_type' => 'author', 'name' => 'Xavier' ], + * 'ownPageList' => [ ['_type'=>'page', 'text' => '...'] ] + * ] ); + * + * + * @param string|array $typeOrBeanArray type or bean array to import + * @param integer $num number of beans to dispense + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return array|OODBBean + */ + public static function dispense( $typeOrBeanArray, $num = 1, $alwaysReturnArray = FALSE ) + { + return DispenseHelper::dispense( self::$redbean, $typeOrBeanArray, $num, $alwaysReturnArray ); + } + + /** + * Takes a comma separated list of bean types + * and dispenses these beans. For each type in the list + * you can specify the number of beans to be dispensed. + * + * Usage: + * + * + * list( $book, $page, $text ) = R::dispenseAll( 'book,page,text' ); + * + * + * This will dispense a book, a page and a text. This way you can + * quickly dispense beans of various types in just one line of code. + * + * Usage: + * + * + * list($book, $pages) = R::dispenseAll('book,page*100'); + * + * + * This returns an array with a book bean and then another array + * containing 100 page beans. + * + * @param string $order a description of the desired dispense order using the syntax above + * @param boolean $onlyArrays return only arrays even if amount < 2 + * + * @return array + */ + public static function dispenseAll( $order, $onlyArrays = FALSE ) + { + return DispenseHelper::dispenseAll( self::$redbean, $order, $onlyArrays ); + } + + /** + * Convience method. Tries to find beans of a certain type, + * if no beans are found, it dispenses a bean of that type. + * Note that this function always returns an array. + * + * @param string $type type of bean you are looking for + * @param string $sql SQL code for finding the bean + * @param array $bindings parameters to bind to SQL + * + * @return array + */ + public static function findOrDispense( $type, $sql = NULL, $bindings = array() ) + { + DispenseHelper::checkType( $type ); + return self::$finder->findOrDispense( $type, $sql, $bindings ); + } + + /** + * Same as findOrDispense but returns just one element. + * + * @param string $type type of bean you are looking for + * @param string $sql SQL code for finding the bean + * @param array $bindings parameters to bind to SQL + * + * @return OODBBean + */ + public static function findOneOrDispense( $type, $sql = NULL, $bindings = array() ) + { + DispenseHelper::checkType( $type ); + $arrayOfBeans = self::findOrDispense( $type, $sql, $bindings ); + return reset($arrayOfBeans); + } + + /** + * Finds beans using a type and optional SQL statement. + * As with most Query tools in RedBean you can provide values to + * be inserted in the SQL statement by populating the value + * array parameter; you can either use the question mark notation + * or the slot-notation (:keyname). + * + * Your SQL does not have to start with a WHERE-clause condition. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * @param string $snippet SQL snippet to include in query (for example: FOR UPDATE) + * + * @return array + */ + public static function find( $type, $sql = NULL, $bindings = array(), $snippet = NULL ) + { + if ( $snippet !== NULL ) self::$writer->setSQLSelectSnippet( $snippet ); + return self::$finder->find( $type, $sql, $bindings ); + } + + /** + * Alias for find(). + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findAll( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->find( $type, $sql, $bindings ); + } + + /** + * Like find() but also exports the beans as an array. + * This method will perform a find-operation. For every bean + * in the result collection this method will call the export() method. + * This method returns an array containing the array representations + * of every bean in the result set. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public static function findAndExport( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findAndExport( $type, $sql, $bindings ); + } + + /** + * Like R::find() but returns the first bean only. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public static function findOne( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findOne( $type, $sql, $bindings ); + } + + /** + * @deprecated + * + * Like find() but returns the last bean of the result array. + * Opposite of Finder::findLast(). + * If no beans are found, this method will return NULL. + * + * Please do not use this function, it is horribly ineffective. + * Instead use a reversed ORDER BY clause and a LIMIT 1 with R::findOne(). + * This function should never be used and only remains for + * the sake of backward compatibility. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public static function findLast( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findLast( $type, $sql, $bindings ); + } + + /** + * Finds a BeanCollection using the repository. + * A bean collection can be used to retrieve one bean at a time using + * cursors - this is useful for processing large datasets. A bean collection + * will not load all beans into memory all at once, just one at a time. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return BeanCollection + */ + public static function findCollection( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findCollection( $type, $sql, $bindings ); + } + + /** + * Returns a hashmap with bean arrays keyed by type using an SQL + * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review' + * this method will return movie and review beans. + * + * Example: + * + * + * $stuff = $finder->findMulti('movie,review', ' + * SELECT movie.*, review.* FROM movie + * LEFT JOIN review ON review.movie_id = movie.id'); + * + * + * After this operation, $stuff will contain an entry 'movie' containing all + * movies and an entry named 'review' containing all reviews (all beans). + * You can also pass bindings. + * + * If you want to re-map your beans, so you can use $movie->ownReviewList without + * having RedBeanPHP executing an SQL query you can use the fourth parameter to + * define a selection of remapping closures. + * + * The remapping argument (optional) should contain an array of arrays. + * Each array in the remapping array should contain the following entries: + * + * + * array( + * 'a' => TYPE A + * 'b' => TYPE B + * 'matcher' => MATCHING FUNCTION ACCEPTING A, B and ALL BEANS + * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS + * ) + * + * + * Using this mechanism you can build your own 'preloader' with tiny function + * snippets (and those can be re-used and shared online of course). + * + * Example: + * + * + * array( + * 'a' => 'movie' //define A as movie + * 'b' => 'review' //define B as review + * 'matcher' => function( $a, $b ) { + * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id + * } + * 'do' => function( $a, $b ) { + * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie + * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'. + * } + * ) + * + * + * @note the SQL query provided IS NOT THE ONE used internally by this function, + * this function will pre-process the query to get all the data required to find the beans. + * + * @note if you use the 'book.*' notation make SURE you're + * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because + * it's actually an SQL-like template SLOT, not real SQL. + * + * @note instead of an SQL query you can pass a result array as well. + * + * @param string|array $types a list of types (either array or comma separated string) + * @param string|array $sql an SQL query or an array of prefetched records + * @param array $bindings optional, bindings for SQL query + * @param array $remappings optional, an array of remapping arrays + * + * @return array + */ + public static function findMulti( $types, $sql, $bindings = array(), $remappings = array() ) + { + return self::$finder->findMulti( $types, $sql, $bindings, $remappings ); + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public static function batch( $type, $ids ) + { + return self::$redbean->batch( $type, $ids ); + } + + /** + * Alias for batch(). Batch method is older but since we added so-called *All + * methods like storeAll, trashAll, dispenseAll and findAll it seemed logical to + * improve the consistency of the Facade API and also add an alias for batch() called + * loadAll. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public static function loadAll( $type, $ids ) + { + return self::$redbean->batch( $type, $ids ); + } + + /** + * Convenience function to execute Queries directly. + * Executes SQL. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return integer + */ + public static function exec( $sql, $bindings = array() ) + { + return self::query( 'exec', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns all rows + * and all columns. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAll( $sql, $bindings = array() ) + { + return self::query( 'get', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single cell. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return string + */ + public static function getCell( $sql, $bindings = array() ) + { + return self::query( 'getCell', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a PDOCursor instance. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return RedBeanPHP\Cursor\PDOCursor + */ + public static function getCursor( $sql, $bindings = array() ) + { + return self::query( 'getCursor', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single row. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getRow( $sql, $bindings = array() ) + { + return self::query( 'getRow', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single column. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getCol( $sql, $bindings = array() ) + { + return self::query( 'getCol', $sql, $bindings ); + } + + /** + * Convenience function to execute Queries directly. + * Executes SQL. + * Results will be returned as an associative array. The first + * column in the select clause will be used for the keys in this array and + * the second column will be used for the values. If only one column is + * selected in the query, both key and value of the array will have the + * value of this field for each row. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAssoc( $sql, $bindings = array() ) + { + return self::query( 'getAssoc', $sql, $bindings ); + } + + /** + *Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns an associative array. + * Results will be returned as an associative array indexed by the first + * column in the select. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAssocRow( $sql, $bindings = array() ) + { + return self::query( 'getAssocRow', $sql, $bindings ); + } + + /** + * Returns the insert ID for databases that support/require this + * functionality. Alias for R::getAdapter()->getInsertID(). + * + * @return mixed + */ + public static function getInsertID() + { + return self::$adapter->getInsertID(); + } + + /** + * Makes a copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following features. + * - All beans in own-lists will be duplicated as well + * - All references to shared beans will be copied but not the shared beans themselves + * - All references to parent objects (_id fields) will be copied but not the parents themselves + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * @deprecated + * This function is deprecated in favour of R::duplicate(). + * This function has a confusing method signature, the R::duplicate() function + * only accepts two arguments: bean and filters. + * + * @param OODBBean $bean bean to be copied + * @param array $trail for internal usage, pass array() + * @param boolean $pid for internal usage + * @param array $filters white list filter with bean types to duplicate + * + * @return array + */ + public static function dup( $bean, $trail = array(), $pid = FALSE, $filters = array() ) + { + self::$duplicationManager->setFilters( $filters ); + return self::$duplicationManager->dup( $bean, $trail, $pid ); + } + + /** + * Makes a deep copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following: + * + * * All beans in own-lists will be duplicated as well + * * All references to shared beans will be copied but not the shared beans themselves + * * All references to parent objects (_id fields) will be copied but not the parents themselves + * + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * Note: + * This is a simplified version of the deprecated R::dup() function. + * + * @param OODBBean $bean bean to be copied + * @param array $white white list filter with bean types to duplicate + * + * @return array + */ + public static function duplicate( $bean, $filters = array() ) + { + return self::dup( $bean, array(), FALSE, $filters ); + } + + /** + * Exports a collection of beans. Handy for XML/JSON exports with a + * Javascript framework like Dojo or ExtJS. + * What will be exported: + * + * * contents of the bean + * * all own bean lists (recursively) + * * all shared beans (not THEIR own lists) + * + * @param array|OODBBean $beans beans to be exported + * @param boolean $parents whether you want parent beans to be exported + * @param array $filters whitelist of types + * @param boolean $meta export meta data as well + * + * @return array + */ + public static function exportAll( $beans, $parents = FALSE, $filters = array(), $meta = FALSE ) + { + return self::$duplicationManager->exportAll( $beans, $parents, $filters, self::$exportCaseStyle, $meta ); + } + + /** + * Selects case style for export. + * This will determine the case style for the keys of exported beans (see exportAll). + * The following options are accepted: + * + * * 'default' RedBeanPHP by default enforces Snake Case (i.e. book_id is_valid ) + * * 'camel' Camel Case (i.e. bookId isValid ) + * * 'dolphin' Dolphin Case (i.e. bookID isValid ) Like CamelCase but ID is written all uppercase + * + * @warning RedBeanPHP transforms camelCase to snake_case using a slightly different + * algorithm, it also converts isACL to is_acl (not is_a_c_l) and bookID to book_id. + * Due to information loss this cannot be corrected. However if you might try + * DolphinCase for IDs it takes into account the exception concerning IDs. + * + * @param string $caseStyle case style identifier + * + * @return void + */ + public static function useExportCase( $caseStyle = 'default' ) + { + if ( !in_array( $caseStyle, array( 'default', 'camel', 'dolphin' ) ) ) throw new RedException( 'Invalid case selected.' ); + self::$exportCaseStyle = $caseStyle; + } + + /** + * Converts a series of rows to beans. + * This method converts a series of rows to beans. + * The type of the desired output beans can be specified in the + * first parameter. The second parameter is meant for the database + * result rows. + * + * Usage: + * + * + * $rows = R::getAll( 'SELECT * FROM ...' ) + * $beans = R::convertToBeans( $rows ); + * + * + * As of version 4.3.2 you can specify a meta-mask. + * Data from columns with names starting with the value specified in the mask + * will be transferred to the meta section of a bean (under data.bundle). + * + * + * $rows = R::getAll( 'SELECT FROM... COUNT(*) AS extra_count ...' ); + * $beans = R::convertToBeans( $rows, 'extra_' ); + * $bean = reset( $beans ); + * $data = $bean->getMeta( 'data.bundle' ); + * $extra_count = $data['extra_count']; + * + * + * New in 4.3.2: meta mask. The meta mask is a special mask to send + * data from raw result rows to the meta store of the bean. This is + * useful for bundling additional information with custom queries. + * Values of every column whos name starts with $mask will be + * transferred to the meta section of the bean under key 'data.bundle'. + * + * @param string $type type of beans to produce + * @param array $rows must contain an array of array + * @param string $metamask meta mask to apply (optional) + * + * @return array + */ + public static function convertToBeans( $type, $rows, $metamask = NULL ) + { + return self::$redbean->convertToBeans( $type, $rows, $metamask ); + } + + /** + * Just like converToBeans, but for one bean. + * + * @param string $type type of bean to produce + * @param array $row one row from the database + * @param string $metamask metamask (see convertToBeans) + * + * @return OODBBean|NULL + */ + public static function convertToBean( $type, $row, $metamask = NULL ) + { + if ( !count( $row ) ) return NULL; + $beans = self::$redbean->convertToBeans( $type, array( $row ), $metamask ); + $bean = reset( $beans ); + return $bean; + } + + /** + * Convenience function to 'find' beans from an SQL query. + * Used mostly to obtain a series of beans as well as + * pagination data (to paginate results) and optionally + * other data as well (that should not be considered part of + * a bean). + * + * Example: + * + * $books = R::findFromSQL('book'," + * SELECT *, count(*) OVER() AS total + * FROM book + * WHERE {$filter} + * OFFSET {$from} LIMIT {$to} ", ['total']); + * + * This is the same as doing (example uses PostgreSQL dialect): + * + * $rows = R::getAll(" + * SELECT *, count(*) OVER() AS total + * FROM book + * WHERE {$filter} + * OFFSET {$from} LIMIT {$to} + * ", $params); + * $books = R::convertToBeans('book', $rows, ['total']); + * + * The additional data can be obtained using: + * + * $book->info('total'); + * + * For further details see R::convertToBeans(). + * If you set $autoExtract to TRUE and meta mask is an array, + * an array will be returned containing two nested arrays, the + * first of those nested arrays will contain the meta values + * you requested, the second array will contain the beans. + * + * @param string $type Type of bean to produce + * @param string $sql SQL query snippet to use + * @param array $bindings bindings for query (optional) + * @param mixed $metamask meta mask (optional, defaults to 'extra_') + * @param boolean $autoExtract TRUE to return meta mask values as first item of array + * + * @return array + */ + public static function findFromSQL( $type, $sql, $bindings = array(), $metamask = 'extra_', $autoExtract = false) { + $rows = self::query( 'get', $sql, $bindings ); + $beans = array(); + if (count($rows)) $beans = self::$redbean->convertToBeans( $type, $rows, $metamask ); + if ($autoExtract && is_array($metamask)) { + $values = array(); + $firstBean = NULL; + if (count($beans)) $firstBean = reset($beans); + foreach($metamask as $key) { + $values[$key] = ($firstBean) ? $firstBean->info($key) : NULL; + } + return array( $values, $beans ); + } + return $beans; + } + + /** + * Tests whether a bean has been associated with one ore more + * of the listed tags. If the third parameter is TRUE this method + * will return TRUE only if all tags that have been specified are indeed + * associated with the given bean, otherwise FALSE. + * If the third parameter is FALSE this + * method will return TRUE if one of the tags matches, FALSE if none + * match. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::hasTag( $blog, 'horror,movie', TRUE ); + * + * + * The example above returns TRUE if the $blog bean has been tagged + * as BOTH horror and movie. If the post has only been tagged as 'movie' + * or 'horror' this operation will return FALSE because the third parameter + * has been set to TRUE. + * + * @param OODBBean $bean bean to check for tags + * @param array|string $tags list of tags + * @param boolean $all whether they must all match or just some + * + * @return boolean + */ + public static function hasTag( $bean, $tags, $all = FALSE ) + { + return self::$tagManager->hasTag( $bean, $tags, $all ); + } + + /** + * Removes all specified tags from the bean. The tags specified in + * the second parameter will no longer be associated with the bean. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::untag( $blog, 'smart,interesting' ); + * + * + * In the example above, the $blog bean will no longer + * be associated with the tags 'smart' and 'interesting'. + * + * @param OODBBean $bean tagged bean + * @param array $tagList list of tags (names) + * + * @return void + */ + public static function untag( $bean, $tagList ) + { + self::$tagManager->untag( $bean, $tagList ); + } + + /** + * Tags a bean or returns tags associated with a bean. + * If $tagList is NULL or omitted this method will return a + * comma separated list of tags associated with the bean provided. + * If $tagList is a comma separated list (string) of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::tag( $meal, "TexMex,Mexican" ); + * $tags = R::tag( $meal ); + * + * + * The first line in the example above will tag the $meal + * as 'TexMex' and 'Mexican Cuisine'. The second line will + * retrieve all tags attached to the meal object. + * + * @param OODBBean $bean bean to tag + * @param mixed $tagList tags to attach to the specified bean + * + * @return string + */ + public static function tag( OODBBean $bean, $tagList = NULL ) + { + return self::$tagManager->tag( $bean, $tagList ); + } + + /** + * Adds tags to a bean. + * If $tagList is a comma separated list of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::addTags( $blog, ["halloween"] ); + * + * + * The example adds the tag 'halloween' to the $blog + * bean. + * + * @param OODBBean $bean bean to tag + * @param array $tagList list of tags to add to bean + * + * @return void + */ + public static function addTags( OODBBean $bean, $tagList ) + { + self::$tagManager->addTags( $bean, $tagList ); + } + + /** + * Returns all beans that have been tagged with one or more + * of the specified tags. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::tagged( + * 'movie', + * 'horror,gothic', + * ' ORDER BY movie.title DESC LIMIT ?', + * [ 10 ] + * ); + * + * + * The example uses R::tagged() to find all movies that have been + * tagged as 'horror' or 'gothic', order them by title and limit + * the number of movies to be returned to 10. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional SQL (use only for pagination) + * @param array $bindings bindings + * + * @return array + */ + public static function tagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->tagged( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Returns all beans that have been tagged with ALL of the tags given. + * This method works the same as R::tagged() except that this method only returns + * beans that have been tagged with all the specified labels. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::taggedAll( + * 'movie', + * [ 'gothic', 'short' ], + * ' ORDER BY movie.id DESC LIMIT ? ', + * [ 4 ] + * ); + * + * + * The example above returns at most 4 movies (due to the LIMIT clause in the SQL + * Query Snippet) that have been tagged as BOTH 'short' AND 'gothic'. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return array + */ + public static function taggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->taggedAll( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Same as taggedAll() but counts beans only (does not return beans). + * + * @see R::taggedAll + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public static function countTaggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->countTaggedAll( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Same as tagged() but counts beans only (does not return beans). + * + * @see R::tagged + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public static function countTagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->countTagged( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Wipes all beans of type $beanType. + * + * @param string $beanType type of bean you want to destroy entirely + * + * @return boolean + */ + public static function wipe( $beanType ) + { + return Facade::$redbean->wipe( $beanType ); + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public static function count( $type, $addSQL = '', $bindings = array() ) + { + return Facade::$redbean->count( $type, $addSQL, $bindings ); + } + + /** + * Configures the facade, want to have a new Writer? A new Object Database or a new + * Adapter and you want it on-the-fly? Use this method to hot-swap your facade with a new + * toolbox. + * + * @param ToolBox $tb toolbox to configure facade with + * + * @return ToolBox + */ + public static function configureFacadeWithToolbox( ToolBox $tb ) + { + $oldTools = self::$toolbox; + self::$toolbox = $tb; + self::$writer = self::$toolbox->getWriter(); + self::$adapter = self::$toolbox->getDatabaseAdapter(); + self::$redbean = self::$toolbox->getRedBean(); + self::$finder = new Finder( self::$toolbox ); + self::$associationManager = new AssociationManager( self::$toolbox ); + self::$tree = new Tree( self::$toolbox ); + self::$redbean->setAssociationManager( self::$associationManager ); + self::$labelMaker = new LabelMaker( self::$toolbox ); + $helper = new SimpleModelHelper(); + $helper->attachEventListeners( self::$redbean ); + if (self::$redbean->getBeanHelper() == NULL) { + self::$redbean->setBeanHelper( new SimpleFacadeBeanHelper ); + } + self::$duplicationManager = new DuplicationManager( self::$toolbox ); + self::$tagManager = new TagManager( self::$toolbox ); + return $oldTools; + } + + /** + * Facade Convience method for adapter transaction system. + * Begins a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function begin() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->startTransaction(); + return TRUE; + } + + /** + * Facade Convience method for adapter transaction system. + * Commits a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function commit() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->commit(); + return TRUE; + } + + /** + * Facade Convience method for adapter transaction system. + * Rolls back a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function rollback() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->rollback(); + return TRUE; + } + + /** + * Returns a list of columns. Format of this array: + * array( fieldname => type ) + * Note that this method only works in fluid mode because it might be + * quite heavy on production servers! + * + * @param string $table name of the table (not type) you want to get columns of + * + * @return array + */ + public static function getColumns( $table ) + { + return self::$writer->getColumns( $table ); + } + + /** + * Generates question mark slots for an array of values. + * Given an array and an optional template string this method + * will produce string containing parameter slots for use in + * an SQL query string. + * + * Usage: + * + * + * R::genSlots( array( 'a', 'b' ) ); + * + * + * The statement in the example will produce the string: + * '?,?'. + * + * Another example, using a template string: + * + * + * R::genSlots( array('a', 'b'), ' IN( %s ) ' ); + * + * + * The statement in the example will produce the string: + * ' IN( ?,? ) '. + * + * @param array $array array to generate question mark slots for + * @param string $template template to use + * + * @return string + */ + public static function genSlots( $array, $template = NULL ) + { + return ArrayTool::genSlots( $array, $template ); + } + + /** + * Convenience method to quickly attach parent beans. + * Although usually this can also be done with findMulti(), that + * approach can be a bit verbose sometimes. This convenience method + * uses a default yet overridable SQL snippet to perform the + * operation, leveraging the power of findMulti(). + * + * Usage: + * + * + * $users = R::find('user'); + * $users = R::loadJoined( $users, 'country' ); + * + * + * This is an alternative for: + * + * + * $all = R::findMulti('country', + * R::genSlots( $users, + * 'SELECT country.* FROM country WHERE id IN ( %s )' ), + * array_column( $users, 'country_id' ), + * [Finder::onmap('country', $gebruikers)] + * ); + * + * + * @param array $beans a list of OODBBeans + * @param string $type a type string + * @param string $sqlTemplate an SQL template string for the SELECT-query + * + * @return array + */ + public static function loadJoined( $beans, $type, $sqlTemplate = 'SELECT %s.* FROM %s WHERE id IN (%s)' ) + { + if (!count($beans)) return array(); + $ids = array(); + $key = "{$type}_id"; + foreach( $beans as $bean ) $ids[] = $bean->{$key}; + $result = self::findMulti($type, self::genSlots( $beans,sprintf($sqlTemplate, $type, $type, '%s')), $ids, array( Finder::onmap($type, $beans) ) ); + $bean = reset($beans); + return $result[ $bean->getMeta('type') ]; + } + + /** + * Flattens a multi dimensional bindings array for use with genSlots(). + * + * Usage: + * + * + * R::flat( array( 'a', array( 'b' ), 'c' ) ); + * + * + * produces an array like: [ 'a', 'b', 'c' ] + * + * @param array $array array to flatten + * @param array $result result array parameter (for recursion) + * + * @return array + */ + public static function flat( $array, $result = array() ) + { + return ArrayTool::flat( $array, $result ); + } + + /** + * Nukes the entire database. + * This will remove all schema structures from the database. + * Only works in fluid mode. Be careful with this method. + * + * @warning dangerous method, will remove all tables, columns etc. + * + * @return void + */ + public static function nuke() + { + return self::wipeAll( TRUE ); + } + + /** + * Truncates or drops all database tables/views. + * Empties the database. If the deleteTables flag is set to TRUE + * this function will also remove the database structures. + * The latter only works in fluid mode. + * + * @param boolean $alsoDeleteTables TRUE to clear entire database. + * + * @return void + */ + public static function wipeAll( $alsoDeleteTables = FALSE ) + { + if ( $alsoDeleteTables ) { + if ( !self::$redbean->isFrozen() ) { + self::$writer->wipeAll(); + } + } else { + foreach ( self::$writer->getTables() as $table ) { + self::wipe( $table ); + } + } + } + + /** + * Short hand function to store a set of beans at once, IDs will be + * returned as an array. For information please consult the R::store() + * function. + * A loop saver. + * + * If the second parameter is set to TRUE and + * Hybrid mode is allowed (default OFF for novice), then RedBeanPHP + * will automatically temporarily switch to fluid mode to attempt to store the + * bean in case of an SQLException. + * + * @param array $beans list of beans to be stored + * @param boolean $unfreezeIfNeeded retries in fluid mode in hybrid mode + * + * @return array + */ + public static function storeAll( $beans, $unfreezeIfNeeded = FALSE ) + { + $ids = array(); + foreach ( $beans as $bean ) { + $ids[] = self::store( $bean, $unfreezeIfNeeded ); + } + return $ids; + } + + /** + * Short hand function to trash a set of beans at once. + * For information please consult the R::trash() function. + * A loop saver. + * + * @param array $beans list of beans to be trashed + * + * @return void + */ + public static function trashAll( $beans ) + { + $numberOfDeletion = 0; + foreach ( $beans as $bean ) { + $numberOfDeletion += self::trash( $bean ); + } + return $numberOfDeletion; + } + + /** + * Short hand function to trash a series of beans using + * only IDs. This function combines trashAll and batch loading + * in one call. Note that while this function accepts just + * bean IDs, the beans will still be loaded first. This is because + * the function still respects all the FUSE hooks that may have beeb + * associated with the domain logic associated with these beans. + * If you really want to delete just records from the database use + * a simple DELETE-FROM SQL query instead. + * + * @param string type $type the bean type you wish to trash + * @param string array $ids list of bean IDs + * + * @return void + */ + public static function trashBatch( $type, $ids ) + { + self::trashAll( self::batch( $type, $ids ) ); + } + + /** + * Short hand function to find and trash beans. + * This function combines trashAll and find. + * Given a bean type, a query snippet and optionally some parameter + * bindings, this function will search for the beans described in the + * query and its parameters and then feed them to the trashAll function + * to be trashed. + * + * Note that while this function accepts just + * a bean type and query snippet, the beans will still be loaded first. This is because + * the function still respects all the FUSE hooks that may have been + * associated with the domain logic associated with these beans. + * If you really want to delete just records from the database use + * a simple DELETE-FROM SQL query instead. + * + * Returns the number of beans deleted. + * + * @param string $type bean type to look for in database + * @param string $sqlSnippet an SQL query snippet + * @param array $bindings SQL parameter bindings + * + * @return int + */ + public static function hunt( $type, $sqlSnippet = NULL, $bindings = array() ) + { + $numberOfTrashedBeans = 0; + $beans = self::findCollection( $type, $sqlSnippet, $bindings ); + while( $bean = $beans->next() ) { + self::trash( $bean ); + $numberOfTrashedBeans++; + } + return $numberOfTrashedBeans; + } + + /** + * Toggles Writer Cache. + * Turns the Writer Cache on or off. The Writer Cache is a simple + * query based caching system that may improve performance without the need + * for cache management. This caching system will cache non-modifying queries + * that are marked with special SQL comments. As soon as a non-marked query + * gets executed the cache will be flushed. Only non-modifying select queries + * have been marked therefore this mechanism is a rather safe way of caching, requiring + * no explicit flushes or reloads. Of course this does not apply if you intend to test + * or simulate concurrent querying. + * + * @param boolean $yesNo TRUE to enable cache, FALSE to disable cache + * + * @return void + */ + public static function useWriterCache( $yesNo ) + { + self::getWriter()->setUseCache( $yesNo ); + } + + /** + * A label is a bean with only an id, type and name property. + * This function will dispense beans for all entries in the array. The + * values of the array will be assigned to the name property of each + * individual bean. + * + * @param string $type type of beans you would like to have + * @param array $labels list of labels, names for each bean + * + * @return array + */ + public static function dispenseLabels( $type, $labels ) + { + return self::$labelMaker->dispenseLabels( $type, $labels ); + } + + /** + * Generates and returns an ENUM value. This is how RedBeanPHP handles ENUMs. + * Either returns a (newly created) bean respresenting the desired ENUM + * value or returns a list of all enums for the type. + * + * To obtain (and add if necessary) an ENUM value: + * + * + * $tea->flavour = R::enum( 'flavour:apple' ); + * + * + * Returns a bean of type 'flavour' with name = apple. + * This will add a bean with property name (set to APPLE) to the database + * if it does not exist yet. + * + * To obtain all flavours: + * + * + * R::enum('flavour'); + * + * + * To get a list of all flavour names: + * + * + * R::gatherLabels( R::enum( 'flavour' ) ); + * + * + * @param string $enum either type or type-value + * + * @return array|OODBBean + */ + public static function enum( $enum ) + { + return self::$labelMaker->enum( $enum ); + } + + /** + * Gathers labels from beans. This function loops through the beans, + * collects the values of the name properties of each individual bean + * and stores the names in a new array. The array then gets sorted using the + * default sort function of PHP (sort). + * + * @param array $beans list of beans to loop + * + * @return array + */ + public static function gatherLabels( $beans ) + { + return self::$labelMaker->gatherLabels( $beans ); + } + + /** + * Closes the database connection. + * While database connections are closed automatically at the end of the PHP script, + * closing database connections is generally recommended to improve performance. + * Closing a database connection will immediately return the resources to PHP. + * + * Usage: + * + * + * R::setup( ... ); + * ... do stuff ... + * R::close(); + * + * + * @return void + */ + public static function close() + { + if ( isset( self::$adapter ) ) { + self::$adapter->close(); + } + } + + /** + * Simple convenience function, returns ISO date formatted representation + * of $time. + * + * @param mixed $time UNIX timestamp + * + * @return string + */ + public static function isoDate( $time = NULL ) + { + if ( !$time ) { + $time = time(); + } + + return @date( 'Y-m-d', $time ); + } + + /** + * Simple convenience function, returns ISO date time + * formatted representation + * of $time. + * + * @param mixed $time UNIX timestamp + * + * @return string + */ + public static function isoDateTime( $time = NULL ) + { + if ( !$time ) $time = time(); + return @date( 'Y-m-d H:i:s', $time ); + } + + /** + * Sets the database adapter you want to use. + * The database adapter manages the connection to the database + * and abstracts away database driver specific interfaces. + * + * @param Adapter $adapter Database Adapter for facade to use + * + * @return void + */ + public static function setDatabaseAdapter( Adapter $adapter ) + { + self::$adapter = $adapter; + } + + /** + * Sets the Query Writer you want to use. + * The Query Writer writes and executes database queries using + * the database adapter. It turns RedBeanPHP 'commands' into + * database 'statements'. + * + * @param QueryWriter $writer Query Writer instance for facade to use + * + * @return void + */ + public static function setWriter( QueryWriter $writer ) + { + self::$writer = $writer; + } + + /** + * Sets the OODB you want to use. + * The RedBeanPHP Object oriented database is the main RedBeanPHP + * interface that allows you to store and retrieve RedBeanPHP + * objects (i.e. beans). + * + * @param OODB $redbean Object Database for facade to use + */ + public static function setRedBean( OODB $redbean ) + { + self::$redbean = $redbean; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return DBAdapter + */ + public static function getDatabaseAdapter() + { + return self::$adapter; + } + + /** + * In case you use PDO (which is recommended and the default but not mandatory, hence + * the database adapter), you can use this method to obtain the PDO object directly. + * This is a convenience method, it will do the same as: + * + * + * R::getDatabaseAdapter()->getDatabase()->getPDO(); + * + * + * If the PDO object could not be found, for whatever reason, this method + * will return NULL instead. + * + * @return NULL|PDO + */ + public static function getPDO() + { + $databaseAdapter = self::getDatabaseAdapter(); + if ( is_null( $databaseAdapter ) ) return NULL; + $database = $databaseAdapter->getDatabase(); + if ( is_null( $database ) ) return NULL; + if ( !method_exists( $database, 'getPDO' ) ) return NULL; + return $database->getPDO(); + } + + /** + * Returns the current duplication manager instance. + * + * @return DuplicationManager + */ + public static function getDuplicationManager() + { + return self::$duplicationManager; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return QueryWriter + */ + public static function getWriter() + { + return self::$writer; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return OODB + */ + public static function getRedBean() + { + return self::$redbean; + } + + /** + * Returns the toolbox currently used by the facade. + * To set the toolbox use R::setup() or R::configureFacadeWithToolbox(). + * To create a toolbox use Setup::kickstart(). Or create a manual + * toolbox using the ToolBox class. + * + * @return ToolBox + */ + public static function getToolBox() + { + return self::$toolbox; + } + + /** + * Mostly for internal use, but might be handy + * for some users. + * This returns all the components of the currently + * selected toolbox. + * + * Returns the components in the following order: + * + * # OODB instance (getRedBean()) + * # Database Adapter + * # Query Writer + * # Toolbox itself + * + * @return array + */ + public static function getExtractedToolbox() + { + return array( self::$redbean, self::$adapter, self::$writer, self::$toolbox ); + } + + /** + * Facade method for AQueryWriter::renameAssociation() + * + * @param string|array $from + * @param string $to + * + * @return void + */ + public static function renameAssociation( $from, $to = NULL ) + { + AQueryWriter::renameAssociation( $from, $to ); + } + + /** + * Little helper method for Resty Bean Can server and others. + * Takes an array of beans and exports each bean. + * Unlike exportAll this method does not recurse into own lists + * and shared lists, the beans are exported as-is, only loaded lists + * are exported. + * + * @param array $beans beans + * + * @return array + */ + public static function beansToArray( $beans ) + { + $list = array(); + foreach( $beans as $bean ) $list[] = $bean->export(); + return $list; + } + + /** + * Sets the error mode for FUSE. + * What to do if a FUSE model method does not exist? + * You can set the following options: + * + * * OODBBean::C_ERR_IGNORE (default), ignores the call, returns NULL + * * OODBBean::C_ERR_LOG, logs the incident using error_log + * * OODBBean::C_ERR_NOTICE, triggers a E_USER_NOTICE + * * OODBBean::C_ERR_WARN, triggers a E_USER_WARNING + * * OODBBean::C_ERR_EXCEPTION, throws an exception + * * OODBBean::C_ERR_FUNC, allows you to specify a custom handler (function) + * * OODBBean::C_ERR_FATAL, triggers a E_USER_ERROR + * + * + * Custom handler method signature: handler( array ( + * 'message' => string + * 'bean' => OODBBean + * 'method' => string + * ) ) + * + * + * This method returns the old mode and handler as an array. + * + * @param integer $mode mode, determines how to handle errors + * @param callable|NULL $func custom handler (if applicable) + * + * @return array + */ + public static function setErrorHandlingFUSE( $mode, $func = NULL ) + { + return OODBBean::setErrorHandlingFUSE( $mode, $func ); + } + + /** + * Dumps bean data to array. + * Given a one or more beans this method will + * return an array containing first part of the string + * representation of each item in the array. + * + * Usage: + * + * + * echo R::dump( $bean ); + * + * + * The example shows how to echo the result of a simple + * dump. This will print the string representation of the + * specified bean to the screen, limiting the output per bean + * to 35 characters to improve readability. Nested beans will + * also be dumped. + * + * @param OODBBean|array $data either a bean or an array of beans + * + * @return array + */ + public static function dump( $data ) + { + return Dump::dump( $data ); + } + + /** + * Binds an SQL function to a column. + * This method can be used to setup a decode/encode scheme or + * perform UUID insertion. This method is especially useful for handling + * MySQL spatial columns, because they need to be processed first using + * the asText/GeomFromText functions. + * + * Example: + * + * + * R::bindFunc( 'read', 'location.point', 'asText' ); + * R::bindFunc( 'write', 'location.point', 'GeomFromText' ); + * + * + * Passing NULL as the function will reset (clear) the function + * for this column/mode. + * + * @param string $mode mode for function: i.e. read or write + * @param string $field field (table.column) to bind function to + * @param string $function SQL function to bind to specified column + * @param boolean $isTemplate TRUE if $function is an SQL string, FALSE for just a function name + * + * @return void + */ + public static function bindFunc( $mode, $field, $function, $isTemplate = FALSE ) + { + self::$redbean->bindFunc( $mode, $field, $function, $isTemplate ); + } + + /** + * Sets global aliases. + * Registers a batch of aliases in one go. This works the same as + * fetchAs 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. + * + * @param array $list list of global aliases to use + * + * @return void + */ + public static function aliases( $list ) + { + OODBBean::aliases( $list ); + } + + /** + * Tries to find a bean matching a certain type and + * criteria set. If no beans are found a new bean + * will be created, the criteria will be imported into this + * bean and the bean will be stored and returned. + * If multiple beans match the criteria only the first one + * will be returned. + * + * @param string $type type of bean to search for + * @param array $like criteria set describing the bean to search for + * @param boolean $hasBeenCreated set to TRUE if bean has been created + * + * @return OODBBean + */ + public static function findOrCreate( $type, $like = array(), $sql = '', &$hasBeenCreated = false ) + { + return self::$finder->findOrCreate( $type, $like, $sql = '', $hasBeenCreated ); + } + + /** + * Tries to find beans matching the specified type and + * criteria set. + * + * If the optional additional SQL snippet is a condition, it will + * be glued to the rest of the query using the AND operator. + * + * @param string $type type of bean to search for + * @param array $like optional criteria set describing the bean to search for + * @param string $sql optional additional SQL for sorting + * @param array $bindings bindings + * + * @return array + */ + public static function findLike( $type, $like = array(), $sql = '', $bindings = array() ) + { + return self::$finder->findLike( $type, $like, $sql, $bindings ); + } + + /** + * Starts logging queries. + * Use this method to start logging SQL queries being + * executed by the adapter. Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * Usage: + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @return void + */ + public static function startLogging() + { + self::debug( TRUE, RDefault::C_LOGGER_ARRAY ); + } + + /** + * Stops logging and flushes the logs, + * convient method to stop logging of queries. + * Use this method to stop logging SQL queries being + * executed by the adapter. Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @note by stopping the logging you also flush the logs. + * Therefore, only stop logging AFTER you have obtained the + * query logs using R::getLogs() + * + * @return void + */ + public static function stopLogging() + { + self::debug( FALSE ); + } + + /** + * Returns the log entries written after the startLogging. + * + * Use this method to obtain the query logs gathered + * by the logging mechanisms. + * Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * The logs may look like: + * + * [1] => SELECT `book`.* FROM `book` WHERE id > ? -- keep-cache + * [2] => array ( 0 => 0, ) + * [3] => resultset: 1 rows + * + * Basically, element in the array is a log entry. + * Parameter bindings are represented as nested arrays (see 2). + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @note by stopping the logging you also flush the logs. + * Therefore, only stop logging AFTER you have obtained the + * query logs using R::getLogs() + * + * @return array + */ + public static function getLogs() + { + return self::getLogger()->getLogs(); + } + + /** + * Resets the query counter. + * The query counter can be used to monitor the number + * of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * R::resetQueryCount(); + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return void + */ + public static function resetQueryCount() + { + self::$adapter->getDatabase()->resetCounter(); + } + + /** + * Returns the number of SQL queries processed. + * This method returns the number of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return integer + */ + public static function getQueryCount() + { + return self::$adapter->getDatabase()->getQueryCount(); + } + + /** + * Returns the current logger instance being used by the + * database object. + * + * @return Logger + */ + public static function getLogger() + { + return self::$adapter->getDatabase()->getLogger(); + } + + /** + * @deprecated + */ + public static function setAutoResolve( $automatic = TRUE ){} + + /** + * Toggles 'partial bean mode'. If this mode has been + * selected the repository will only update the fields of a bean that + * have been changed rather than the entire bean. + * Pass the value TRUE to select 'partial mode' for all beans. + * Pass the value FALSE to disable 'partial mode'. + * Pass an array of bean types if you wish to use partial mode only + * for some types. + * This method will return the previous value. + * + * @param boolean|array $yesNoBeans List of type names or 'all' + * + * @return mixed + */ + public static function usePartialBeans( $yesNoBeans ) + { + return self::$redbean->getCurrentRepository()->usePartialBeans( $yesNoBeans ); + } + + /** + * Exposes the result of the specified SQL query as a CSV file. + * + * Usage: + * + * + * R::csv( 'SELECT + * `name`, + * population + * FROM city + * WHERE region = :region ', + * array( ':region' => 'Denmark' ), + * array( 'city', 'population' ), + * '/tmp/cities.csv' + * ); + * + * + * The command above will select all cities in Denmark + * and create a CSV with columns 'city' and 'population' and + * populate the cells under these column headers with the + * names of the cities and the population numbers respectively. + * + * @param string $sql SQL query to expose result of + * @param array $bindings parameter bindings + * @param array $columns column headers for CSV file + * @param string $path path to save CSV file to + * @param boolean $output TRUE to output CSV directly using readfile + * @param array $options delimiter, quote and escape character respectively + * + * @return void + */ + public static function csv( $sql = '', $bindings = array(), $columns = NULL, $path = '/tmp/redexport_%s.csv', $output = TRUE ) + { + $quickExport = new QuickExport( self::$toolbox ); + $quickExport->csv( $sql, $bindings, $columns, $path, $output ); + } + + /** + * MatchUp is a powerful productivity boosting method that can replace simple control + * scripts with a single RedBeanPHP command. Typically, matchUp() is used to + * replace login scripts, token generation scripts and password reset scripts. + * The MatchUp method takes a bean type, an SQL query snippet (starting at the WHERE clause), + * SQL bindings, a pair of task arrays and a bean reference. + * + * If the first 3 parameters match a bean, the first task list will be considered, + * otherwise the second one will be considered. On consideration, each task list, + * an array of keys and values will be executed. Every key in the task list should + * correspond to a bean property while every value can either be an expression to + * be evaluated or a closure (PHP 5.3+). After applying the task list to the bean + * it will be stored. If no bean has been found, a new bean will be dispensed. + * + * This method will return TRUE if the bean was found and FALSE if not AND + * there was a NOT-FOUND task list. If no bean was found AND there was also + * no second task list, NULL will be returned. + * + * To obtain the bean, pass a variable as the sixth parameter. + * The function will put the matching bean in the specified variable. + * + * @param string $type type of bean you're looking for + * @param string $sql SQL snippet (starting at the WHERE clause, omit WHERE-keyword) + * @param array $bindings array of parameter bindings for SQL snippet + * @param array $onFoundDo task list to be considered on finding the bean + * @param array $onNotFoundDo task list to be considered on NOT finding the bean + * @param OODBBean &$bean reference to obtain the found bean + * + * @return mixed + */ + public static function matchUp( $type, $sql, $bindings = array(), $onFoundDo = NULL, $onNotFoundDo = NULL, &$bean = NULL ) { + $matchUp = new MatchUp( self::$toolbox ); + return $matchUp->matchUp( $type, $sql, $bindings, $onFoundDo, $onNotFoundDo, $bean ); + } + + /** + * @deprecated + * + * Returns an instance of the Look Helper class. + * The instance will be configured with the current toolbox. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * For more details regarding the Look functionality, please consult R::look(). + * @see Facade::look + * @see Look::look + * + * @return Look + */ + public static function getLook() + { + return new Look( self::$toolbox ); + } + + /** + * Takes an full SQL query with optional bindings, a series of keys, a template + * and optionally a filter function and glue and assembles a view from all this. + * This is the fastest way from SQL to view. Typically this function is used to + * generate pulldown (select tag) menus with options queried from the database. + * + * Usage: + * + * + * $htmlPulldown = R::look( + * 'SELECT * FROM color WHERE value != ? ORDER BY value ASC', + * [ 'g' ], + * [ 'value', 'name' ], + * '', + * 'strtoupper', + * "\n" + * ); + * + * + * The example above creates an HTML fragment like this: + * + * + * + * + * to pick a color from a palette. The HTML fragment gets constructed by + * an SQL query that selects all colors that do not have value 'g' - this + * excludes green. Next, the bean properties 'value' and 'name' are mapped to the + * HTML template string, note that the order here is important. The mapping and + * the HTML template string follow vsprintf-rules. All property values are then + * passed through the specified filter function 'strtoupper' which in this case + * is a native PHP function to convert strings to uppercase characters only. + * Finally the resulting HTML fragment strings are glued together using a + * newline character specified in the last parameter for readability. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * @param string $sql query to execute + * @param array $bindings parameters to bind to slots mentioned in query or an empty array + * @param array $keys names in result collection to map to template + * @param string $template HTML template to fill with values associated with keys, use printf notation (i.e. %s) + * @param callable $filter function to pass values through (for translation for instance) + * @param string $glue optional glue to use when joining resulting strings + * + * @return string + */ + public static function look( $sql, $bindings = array(), $keys = array( 'selected', 'id', 'name' ), $template = '', $filter = 'trim', $glue = '' ) + { + return self::getLook()->look( $sql, $bindings, $keys, $template, $filter, $glue ); + } + + /** + * Calculates a diff between two beans (or arrays of beans). + * The result of this method is an array describing the differences of the second bean compared to + * the first, where the first bean is taken as reference. The array is keyed by type/property, id and property name, where + * type/property is either the type (in case of the root bean) or the property of the parent bean where the type resides. + * The diffs are mainly intended for logging, you cannot apply these diffs as patches to other beans. + * However this functionality might be added in the future. + * + * The keys of the array can be formatted using the $format parameter. + * A key will be composed of a path (1st), id (2nd) and property (3rd). + * Using printf-style notation you can determine the exact format of the key. + * The default format will look like: + * + * 'book.1.title' => array( , ) + * + * If you only want a simple diff of one bean and you don't care about ids, + * you might pass a format like: '%1$s.%3$s' which gives: + * + * 'book.1.title' => array( , ) + * + * The filter parameter can be used to set filters, it should be an array + * of property names that have to be skipped. By default this array is filled with + * two strings: 'created' and 'modified'. + * + * @param OODBBean|array $bean reference beans + * @param OODBBean|array $other beans to compare + * @param array $filters names of properties of all beans to skip + * @param string $format the format of the key, defaults to '%s.%s.%s' + * @param string $type type/property of bean to use for key generation + * + * @return array + */ + public static function diff( $bean, $other, $filters = array( 'created', 'modified' ), $pattern = '%s.%s.%s' ) + { + $diff = new Diff( self::$toolbox ); + return $diff->diff( $bean, $other, $filters, $pattern ); + } + + /** + * The gentleman's way to register a RedBeanPHP ToolBox instance + * with the facade. Stores the toolbox in the static toolbox + * registry of the facade class. This allows for a neat and + * explicit way to register a toolbox. + * + * @param string $key key to store toolbox instance under + * @param ToolBox $toolbox toolbox to register + * + * @return void + */ + public static function addToolBoxWithKey( $key, ToolBox $toolbox ) + { + self::$toolboxes[$key] = $toolbox; + } + + /** + * The gentleman's way to remove a RedBeanPHP ToolBox instance + * from the facade. Removes the toolbox identified by + * the specified key in the static toolbox + * registry of the facade class. This allows for a neat and + * explicit way to remove a toolbox. + * Returns TRUE if the specified toolbox was found and removed. + * Returns FALSE otherwise. + * + * @param string $key identifier of the toolbox to remove + * + * @return boolean + */ + public static function removeToolBoxByKey( $key ) + { + if ( !array_key_exists( $key, self::$toolboxes ) ) { + return FALSE; + } + unset( self::$toolboxes[$key] ); + return TRUE; + } + + /** + * Returns the toolbox associated with the specified key. + * + * @param string $key key to store toolbox instance under + * @param ToolBox $toolbox toolbox to register + * + * @return ToolBox|NULL + */ + public static function getToolBoxByKey( $key ) + { + if ( !array_key_exists( $key, self::$toolboxes ) ) { + return NULL; + } + return self::$toolboxes[$key]; + } + + /** + * Toggles JSON column features. + * Invoking this method with boolean TRUE causes 2 JSON features to be enabled. + * Beans will automatically JSONify any array that's not in a list property and + * the Query Writer (if capable) will attempt to create a JSON column for strings that + * appear to contain JSON. + * + * Feature #1: + * AQueryWriter::useJSONColumns + * + * Toggles support for automatic generation of JSON columns. + * Using JSON columns means that strings containing JSON will + * cause the column to be created (not modified) as a JSON column. + * However it might also trigger exceptions if this means the DB attempts to + * convert a non-json column to a JSON column. + * + * Feature #2: + * OODBBean::convertArraysToJSON + * + * Toggles array to JSON conversion. If set to TRUE any array + * set to a bean property that's not a list will be turned into + * a JSON string. Used together with AQueryWriter::useJSONColumns this + * extends the data type support for JSON columns. + * + * So invoking this method is the same as: + * + * + * AQueryWriter::useJSONColumns( $flag ); + * OODBBean::convertArraysToJSON( $flag ); + * + * + * Unlike the methods above, that return the previous state, this + * method does not return anything (void). + * + * @param boolean $flag feature flag (either TRUE or FALSE) + * + * @return void + */ + public static function useJSONFeatures( $flag ) + { + AQueryWriter::useJSONColumns( $flag ); + OODBBean::convertArraysToJSON( $flag ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will return the bean together with all + * child beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + */ + public static function children( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + return self::$tree->children( $bean, $sql, $bindings ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will count all child beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + */ + public static function countChildren( OODBBean $bean, $sql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_COUNT ) + { + return self::$tree->countChildren( $bean, $sql, $bindings, $select ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will count all parent beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + */ + public static function countParents( OODBBean $bean, $sql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_COUNT ) + { + return self::$tree->countParents( $bean, $sql, $bindings, $select ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will return the bean along with all parent beans + * in a hierarchically structured bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @param OODBBean $bean bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + */ + public static function parents( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + return self::$tree->parents( $bean, $sql, $bindings ); + } + + /** + * Toggles support for nuke(). + * Can be used to turn off the nuke() feature for security reasons. + * Returns the old flag value. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function noNuke( $yesNo ) { + return AQueryWriter::forbidNuke( $yesNo ); + } + + /** + * Selects the feature set you want as specified by + * the label. + * + * Usage: + * + * + * R::useFeatureSet( 'novice/latest' ); + * + * + * @param string $label label + * + * @return void + */ + public static function useFeatureSet( $label ) { + return Feature::feature($label); + } + + /** + * Dynamically extends the facade with a plugin. + * Using this method you can register your plugin with the facade and then + * use the plugin by invoking the name specified plugin name as a method on + * the facade. + * + * Usage: + * + * + * R::ext( 'makeTea', function() { ... } ); + * + * + * Now you can use your makeTea plugin like this: + * + * + * R::makeTea(); + * + * + * @param string $pluginName name of the method to call the plugin + * @param callable $callable a PHP callable + * + * @return void + */ + public static function ext( $pluginName, $callable ) + { + if ( !preg_match( '#^[a-zA-Z_][a-zA-Z0-9_]*$#', $pluginName ) ) { + throw new RedException( 'Plugin name may only contain alphanumeric characters and underscores and cannot start with a number.' ); + } + self::$plugins[$pluginName] = $callable; + } + + /** + * Call static for use with dynamic plugins. This magic method will + * intercept static calls and route them to the specified plugin. + * + * @param string $pluginName name of the plugin + * @param array $params list of arguments to pass to plugin method + * + * @return mixed + */ + public static function __callStatic( $pluginName, $params ) + { + if ( !isset( self::$plugins[$pluginName] ) ) { + if ( !preg_match( '#^[a-zA-Z_][a-zA-Z0-9_]*$#', $pluginName ) ) { + throw new RedException( 'Plugin name may only contain alphanumeric characters and underscores and cannot start with a number.' ); + } + throw new RedException( 'Plugin \''.$pluginName.'\' does not exist, add this plugin using: R::ext(\''.$pluginName.'\')' ); + } + return call_user_func_array( self::$plugins[$pluginName], $params ); + } +} + +} + +namespace RedBeanPHP { + +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\AssociationManager as AssociationManager; +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; + +/** + * Duplication Manager + * The Duplication Manager creates deep copies from beans, this means + * it can duplicate an entire bean hierarchy. You can use this feature to + * implement versioning for instance. Because duplication and exporting are + * closely related this class is also used to export beans recursively + * (i.e. we make a duplicate and then convert to array). This class allows + * you to tune the duplication process by specifying filters determining + * which relations to take into account and by specifying tables + * (in which case no reflective queries have to be issued thus improving + * performance). This class also hosts the Camelfy function used to + * reformat the keys of an array, this method is publicly available and + * used internally by exportAll(). + * + * @file RedBeanPHP/DuplicationManager.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class DuplicationManager +{ + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * @var AssociationManager + */ + protected $associationManager; + + /** + * @var OODB + */ + protected $redbean; + + /** + * @var array + */ + protected $tables = array(); + + /** + * @var array + */ + protected $columns = array(); + + /** + * @var array + */ + protected $filters = array(); + + /** + * @var array + */ + protected $cacheTables = FALSE; + + /** + * @var boolean + */ + protected $copyMeta = FALSE; + + /** + * Copies the shared beans in a bean, i.e. all the sharedBean-lists. + * + * @param OODBBean $copy target bean to copy lists to + * @param string $shared name of the shared list + * @param array $beans array with shared beans to copy + * + * @return void + */ + private function copySharedBeans( OODBBean $copy, $shared, $beans ) + { + $copy->$shared = array(); + + foreach ( $beans as $subBean ) { + array_push( $copy->$shared, $subBean ); + } + } + + /** + * Copies the own beans in a bean, i.e. all the ownBean-lists. + * Each bean in the own-list belongs exclusively to its owner so + * we need to invoke the duplicate method again to duplicate each bean here. + * + * @param OODBBean $copy target bean to copy lists to + * @param string $owned name of the own list + * @param array $beans array with shared beans to copy + * @param array $trail array with former beans to detect recursion + * @param boolean $preserveIDs TRUE means preserve IDs, for export only + * + * @return void + */ + private function copyOwnBeans( OODBBean $copy, $owned, $beans, $trail, $preserveIDs ) + { + $copy->$owned = array(); + foreach ( $beans as $subBean ) { + array_push( $copy->$owned, $this->duplicate( $subBean, $trail, $preserveIDs ) ); + } + } + + /** + * Creates a copy of bean $bean and copies all primitive properties (not lists) + * and the parents beans to the newly created bean. Also sets the ID of the bean + * to 0. + * + * @param OODBBean $bean bean to copy + * + * @return OODBBean + */ + private function createCopy( OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + + $copy = $this->redbean->dispense( $type ); + $copy->setMeta( 'sys.dup-from-id', $bean->id ); + $copy->setMeta( 'sys.old-id', $bean->id ); + $copy->importFrom( $bean ); + if ($this->copyMeta) $copy->copyMetaFrom($bean); + $copy->id = 0; + + return $copy; + } + + /** + * Generates a key from the bean type and its ID and determines if the bean + * occurs in the trail, if not the bean will be added to the trail. + * Returns TRUE if the bean occurs in the trail and FALSE otherwise. + * + * @param array $trail list of former beans + * @param OODBBean $bean currently selected bean + * + * @return boolean + */ + private function inTrailOrAdd( &$trail, OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + $key = $type . $bean->getID(); + + if ( isset( $trail[$key] ) ) { + return TRUE; + } + + $trail[$key] = $bean; + + return FALSE; + } + + /** + * Given the type name of a bean this method returns the canonical names + * of the own-list and the shared-list properties respectively. + * Returns a list with two elements: name of the own-list, and name + * of the shared list. + * + * @param string $typeName bean type name + * + * @return array + */ + private function getListNames( $typeName ) + { + $owned = 'own' . ucfirst( $typeName ); + $shared = 'shared' . ucfirst( $typeName ); + + return array( $owned, $shared ); + } + + /** + * Determines whether the bean has an own list based on + * schema inspection from realtime schema or cache. + * + * @param string $type bean type to get list for + * @param string $target type of list you want to detect + * + * @return boolean + */ + protected function hasOwnList( $type, $target ) + { + return isset( $this->columns[$target][$type . '_id'] ); + } + + /** + * Determines whether the bea has a shared list based on + * schema inspection from realtime schema or cache. + * + * @param string $type bean type to get list for + * @param string $target type of list you are looking for + * + * @return boolean + */ + protected function hasSharedList( $type, $target ) + { + return in_array( AQueryWriter::getAssocTableFormat( array( $type, $target ) ), $this->tables ); + } + + /** + * @see DuplicationManager::dup + * + * @param OODBBean $bean bean to be copied + * @param array $trail trail to prevent infinite loops + * @param boolean $preserveIDs preserve IDs + * + * @return OODBBean + */ + protected function duplicate( OODBBean $bean, $trail = array(), $preserveIDs = FALSE ) + { + if ( $this->inTrailOrAdd( $trail, $bean ) ) return $bean; + + $type = $bean->getMeta( 'type' ); + + $copy = $this->createCopy( $bean ); + foreach ( $this->tables as $table ) { + + if ( !empty( $this->filters ) ) { + if ( !in_array( $table, $this->filters ) ) continue; + } + + list( $owned, $shared ) = $this->getListNames( $table ); + + if ( $this->hasSharedList( $type, $table ) ) { + if ( $beans = $bean->$shared ) { + $this->copySharedBeans( $copy, $shared, $beans ); + } + } elseif ( $this->hasOwnList( $type, $table ) ) { + if ( $beans = $bean->$owned ) { + $this->copyOwnBeans( $copy, $owned, $beans, $trail, $preserveIDs ); + } + + $copy->setMeta( 'sys.shadow.' . $owned, NULL ); + } + + $copy->setMeta( 'sys.shadow.' . $shared, NULL ); + } + + $copy->id = ( $preserveIDs ) ? $bean->id : $copy->id; + + return $copy; + } + + /** + * Constructor, + * creates a new instance of DupManager. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + $this->associationManager = $this->redbean->getAssociationManager(); + } + + /** + * Recursively turns the keys of an array into + * camelCase. + * + * @param array $array array to camelize + * @param boolean $dolphinMode whether you want the exception for IDs. + * + * @return array + */ + public function camelfy( $array, $dolphinMode = FALSE ) { + $newArray = array(); + foreach( $array as $key => $element ) { + $newKey = preg_replace_callback( '/_(\w)/', function( &$matches ){ + return strtoupper( $matches[1] ); + }, $key); + + if ( $dolphinMode ) { + $newKey = preg_replace( '/(\w)Id$/', '$1ID', $newKey ); + } + + $newArray[$newKey] = ( is_array($element) ) ? $this->camelfy( $element, $dolphinMode ) : $element; + } + return $newArray; + } + + /** + * For better performance you can pass the tables in an array to this method. + * If the tables are available the duplication manager will not query them so + * this might be beneficial for performance. + * + * This method allows two array formats: + * + * + * array( TABLE1, TABLE2 ... ) + * + * + * or + * + * + * array( TABLE1 => array( COLUMN1, COLUMN2 ... ) ... ) + * + * + * @param array $tables a table cache array + * + * @return void + */ + public function setTables( $tables ) + { + foreach ( $tables as $key => $value ) { + if ( is_numeric( $key ) ) { + $this->tables[] = $value; + } else { + $this->tables[] = $key; + $this->columns[$key] = $value; + } + } + + $this->cacheTables = TRUE; + } + + /** + * Returns a schema array for cache. + * You can use the return value of this method as a cache, + * store it in RAM or on disk and pass it to setTables later. + * + * @return array + */ + public function getSchema() + { + return $this->columns; + } + + /** + * Indicates whether you want the duplication manager to cache the database schema. + * If this flag is set to TRUE the duplication manager will query the database schema + * only once. Otherwise the duplicationmanager will, by default, query the schema + * every time a duplication action is performed (dup()). + * + * @param boolean $yesNo TRUE to use caching, FALSE otherwise + */ + public function setCacheTables( $yesNo ) + { + $this->cacheTables = $yesNo; + } + + /** + * A filter array is an array with table names. + * By setting a table filter you can make the duplication manager only take into account + * certain bean types. Other bean types will be ignored when exporting or making a + * deep copy. If no filters are set all types will be taking into account, this is + * the default behavior. + * + * @param array $filters list of tables to be filtered + * + * @return void + */ + public function setFilters( $filters ) + { + if ( !is_array( $filters ) ) { + $filters = array( $filters ); + } + + $this->filters = $filters; + } + + /** + * Makes a copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following features. + * - All beans in own-lists will be duplicated as well + * - All references to shared beans will be copied but not the shared beans themselves + * - All references to parent objects (_id fields) will be copied but not the parents themselves + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * Note: + * this function actually passes the arguments to a protected function called + * duplicate() that does all the work. This method takes care of creating a clone + * of the bean to avoid the bean getting tainted (triggering saving when storing it). + * + * @param OODBBean $bean bean to be copied + * @param array $trail for internal usage, pass array() + * @param boolean $preserveIDs for internal usage + * + * @return OODBBean + */ + public function dup( OODBBean $bean, $trail = array(), $preserveIDs = FALSE ) + { + if ( !count( $this->tables ) ) { + $this->tables = $this->toolbox->getWriter()->getTables(); + } + + if ( !count( $this->columns ) ) { + foreach ( $this->tables as $table ) { + $this->columns[$table] = $this->toolbox->getWriter()->getColumns( $table ); + } + } + + $rs = $this->duplicate( ( clone $bean ), $trail, $preserveIDs ); + + if ( !$this->cacheTables ) { + $this->tables = array(); + $this->columns = array(); + } + + return $rs; + } + + /** + * Exports a collection of beans recursively. + * This method will export an array of beans in the first argument to a + * set of arrays. This can be used to send JSON or XML representations + * of bean hierarchies to the client. + * + * For every bean in the array this method will export: + * + * - contents of the bean + * - all own bean lists (recursively) + * - all shared beans (but not THEIR own lists) + * + * If the second parameter is set to TRUE the parents of the beans in the + * array will be exported as well (but not THEIR parents). + * + * The third parameter can be used to provide a white-list array + * for filtering. This is an array of strings representing type names, + * only the type names in the filter list will be exported. + * + * The fourth parameter can be used to change the keys of the resulting + * export arrays. The default mode is 'snake case' but this leaves the + * keys as-is, because 'snake' is the default case style used by + * RedBeanPHP in the database. You can set this to 'camel' for + * camel cased keys or 'dolphin' (same as camelcase but id will be + * converted to ID instead of Id). + * + * @param array|OODBBean $beans beans to be exported + * @param boolean $parents also export parents + * @param array $filters only these types (whitelist) + * @param string $caseStyle case style identifier + * @param boolean $meta export meta data as well + * + * @return array + */ + public function exportAll( $beans, $parents = FALSE, $filters = array(), $caseStyle = 'snake', $meta = FALSE) + { + $array = array(); + if ( !is_array( $beans ) ) { + $beans = array( $beans ); + } + $this->copyMeta = $meta; + foreach ( $beans as $bean ) { + $this->setFilters( $filters ); + $duplicate = $this->dup( $bean, array(), TRUE ); + $array[] = $duplicate->export( $meta, $parents, FALSE, $filters ); + } + if ( $caseStyle === 'camel' ) $array = $this->camelfy( $array ); + if ( $caseStyle === 'dolphin' ) $array = $this->camelfy( $array, TRUE ); + return $array; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\RedException as RedException; + +/** + * Array Tool Helper + * + * This code was originally part of the facade, however it has + * been decided to remove unique features to service classes like + * this to make them available to developers not using the facade class. + * + * This is a helper or service class containing frequently used + * array functions for dealing with SQL queries. + * + * @file RedBeanPHP/Util/ArrayTool.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class ArrayTool +{ + /** + * Generates question mark slots for an array of values. + * Given an array and an optional template string this method + * will produce string containing parameter slots for use in + * an SQL query string. + * + * Usage: + * + * + * R::genSlots( array( 'a', 'b' ) ); + * + * + * The statement in the example will produce the string: + * '?,?'. + * + * Another example, using a template string: + * + * + * R::genSlots( array('a', 'b'), ' IN( %s ) ' ); + * + * + * The statement in the example will produce the string: + * ' IN( ?,? ) '. + * + * @param array $array array to generate question mark slots for + * @param string $template template to use + * + * @return string + */ + public static function genSlots( $array, $template = NULL ) + { + $str = count( $array ) ? implode( ',', array_fill( 0, count( $array ), '?' ) ) : ''; + return ( is_null( $template ) || $str === '' ) ? $str : sprintf( $template, $str ); + } + + /** + * Flattens a multi dimensional bindings array for use with genSlots(). + * + * Usage: + * + * + * R::flat( array( 'a', array( 'b' ), 'c' ) ); + * + * + * produces an array like: [ 'a', 'b', 'c' ] + * + * @param array $array array to flatten + * @param array $result result array parameter (for recursion) + * + * @return array + */ + public static function flat( $array, $result = array() ) + { + foreach( $array as $value ) { + if ( is_array( $value ) ) $result = self::flat( $value, $result ); + else $result[] = $value; + } + return $result; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\RedException as RedException; + +/** + * Dispense Helper + * + * A helper class containing a dispense utility. + * + * @file RedBeanPHP/Util/DispenseHelper.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class DispenseHelper +{ + /** + * @var boolean + */ + private static $enforceNamingPolicy = TRUE; + + /** + * Sets the enforce naming policy flag. If set to + * TRUE the RedBeanPHP naming policy will be enforced. + * Otherwise it will not. Use at your own risk. + * Setting this to FALSE is not recommended. + * + * @param boolean $yesNo whether to enforce RB name policy + * + * @return void + */ + public static function setEnforceNamingPolicy( $yesNo ) + { + self::$enforceNamingPolicy = (boolean) $yesNo; + } + + /** + * Checks whether the bean type conforms to the RedbeanPHP + * naming policy. This method will throw an exception if the + * type does not conform to the RedBeanPHP database column naming + * policy. + * + * The RedBeanPHP naming policy for beans states that valid + * bean type names contain only: + * + * - lowercase alphanumeric characters a-z + * - numbers 0-9 + * - at least one character + * + * Although there are no restrictions on length, database + * specific implementations may apply further restrictions + * regarding the length of a table which means these restrictions + * also apply to bean types. + * + * The RedBeanPHP naming policy ensures that, without any + * configuration, the core functionalities work across many + * databases and operating systems, including those that are + * case insensitive or restricted to the ASCII character set. + * + * Although these restrictions can be bypassed, this is not + * recommended. + * + * @param string $type type of bean + * + * @return void + */ + public static function checkType( $type ) + { + if ( !preg_match( '/^[a-z0-9]+$/', $type ) ) { + throw new RedException( 'Invalid type: ' . $type ); + } + } + + /** + * Dispenses a new RedBean OODB Bean for use with + * the rest of the methods. RedBeanPHP thinks in beans, the bean is the + * primary way to interact with RedBeanPHP and the database managed by + * RedBeanPHP. To load, store and delete data from the database using RedBeanPHP + * you exchange these RedBeanPHP OODB Beans. The only exception to this rule + * are the raw query methods like R::getCell() or R::exec() and so on. + * The dispense method is the 'preferred way' to create a new bean. + * + * Usage: + * + * + * $book = R::dispense( 'book' ); + * $book->title = 'My Book'; + * R::store( $book ); + * + * + * This method can also be used to create an entire bean graph at once. + * Given an array with keys specifying the property names of the beans + * and a special _type key to indicate the type of bean, one can + * make the Dispense Helper generate an entire hierarchy of beans, including + * lists. To make dispense() generate a list, simply add a key like: + * ownXList or sharedXList where X is the type of beans it contains and + * a set its value to an array filled with arrays representing the beans. + * Note that, although the type may have been hinted at in the list name, + * you still have to specify a _type key for every bean array in the list. + * Note that, if you specify an array to generate a bean graph, the number + * parameter will be ignored. + * + * Usage: + * + * + * $book = R::dispense( [ + * '_type' => 'book', + * 'title' => 'Gifted Programmers', + * 'author' => [ '_type' => 'author', 'name' => 'Xavier' ], + * 'ownPageList' => [ ['_type'=>'page', 'text' => '...'] ] + * ] ); + * + * + * @param string|array $typeOrBeanArray type or bean array to import + * @param integer $num number of beans to dispense + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return array|OODBBean + */ + public static function dispense( OODB $oodb, $typeOrBeanArray, $num = 1, $alwaysReturnArray = FALSE ) { + + if ( is_array($typeOrBeanArray) ) { + + if ( !isset( $typeOrBeanArray['_type'] ) ) { + $list = array(); + foreach( $typeOrBeanArray as $beanArray ) { + if ( + !( is_array( $beanArray ) + && isset( $beanArray['_type'] ) ) ) { + throw new RedException( 'Invalid Array Bean' ); + } + } + foreach( $typeOrBeanArray as $beanArray ) $list[] = self::dispense( $oodb, $beanArray ); + return $list; + } + + $import = $typeOrBeanArray; + $type = $import['_type']; + unset( $import['_type'] ); + } else { + $type = $typeOrBeanArray; + } + + if (self::$enforceNamingPolicy) self::checkType( $type ); + + $beanOrBeans = $oodb->dispense( $type, $num, $alwaysReturnArray ); + + if ( isset( $import ) ) { + $beanOrBeans->import( $import ); + } + + return $beanOrBeans; + } + + + /** + * Takes a comma separated list of bean types + * and dispenses these beans. For each type in the list + * you can specify the number of beans to be dispensed. + * + * Usage: + * + * + * list( $book, $page, $text ) = R::dispenseAll( 'book,page,text' ); + * + * + * This will dispense a book, a page and a text. This way you can + * quickly dispense beans of various types in just one line of code. + * + * Usage: + * + * + * list($book, $pages) = R::dispenseAll('book,page*100'); + * + * + * This returns an array with a book bean and then another array + * containing 100 page beans. + * + * @param OODB $oodb OODB + * @param string $order a description of the desired dispense order using the syntax above + * @param boolean $onlyArrays return only arrays even if amount < 2 + * + * @return array + */ + public static function dispenseAll( OODB $oodb, $order, $onlyArrays = FALSE ) + { + $list = array(); + + foreach( explode( ',', $order ) as $order ) { + if ( strpos( $order, '*' ) !== FALSE ) { + list( $type, $amount ) = explode( '*', $order ); + } else { + $type = $order; + $amount = 1; + } + + $list[] = self::dispense( $oodb, $type, $amount, $onlyArrays ); + } + + return $list; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; + +/** + * Dump helper + * + * This code was originally part of the facade, however it has + * been decided to remove unique features to service classes like + * this to make them available to developers not using the facade class. + * + * Dumps the contents of a bean in an array for + * debugging purposes. + * + * @file RedBeanPHP/Util/Dump.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Dump +{ + /** + * Dumps bean data to array. + * Given a one or more beans this method will + * return an array containing first part of the string + * representation of each item in the array. + * + * Usage: + * + * + * echo R::dump( $bean ); + * + * + * The example shows how to echo the result of a simple + * dump. This will print the string representation of the + * specified bean to the screen, limiting the output per bean + * to 35 characters to improve readability. Nested beans will + * also be dumped. + * + * @param OODBBean|array $data either a bean or an array of beans + * + * @return array + */ + public static function dump( $data ) + { + $array = array(); + if ( $data instanceof OODBBean ) { + $str = strval( $data ); + if (strlen($str) > 35) { + $beanStr = substr( $str, 0, 35 ).'... '; + } else { + $beanStr = $str; + } + return $beanStr; + } + if ( is_array( $data ) ) { + foreach( $data as $key => $item ) { + $array[$key] = self::dump( $item ); + } + } + return $array; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; + +/** + * Multi Bean Loader Helper + * + * This code was originally part of the facade, however it has + * been decided to remove unique features to service classes like + * this to make them available to developers not using the facade class. + * + * This helper class offers limited support for one-to-one + * relations by providing a service to load a set of beans + * with differnt types and a common ID. + * + * @file RedBeanPHP/Util/MultiLoader.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class MultiLoader +{ + /** + * Loads multiple types of beans with the same ID. + * This might look like a strange method, however it can be useful + * for loading a one-to-one relation. In a typical 1-1 relation, + * you have two records sharing the same primary key. + * RedBeanPHP has only limited support for 1-1 relations. + * In general it is recommended to use 1-N for this. + * + * Usage: + * + * + * list( $author, $bio ) = R::loadMulti( 'author, bio', $id ); + * + * + * @param OODB $oodb OODB object + * @param string|array $types the set of types to load at once + * @param mixed $id the common ID + * + * @return OODBBean + */ + public static function load( OODB $oodb, $types, $id ) + { + if ( is_string( $types ) ) $types = explode( ',', $types ); + if ( !is_array( $types ) ) return array(); + foreach ( $types as $k => $typeItem ) { + $types[$k] = $oodb->load( $typeItem, $id ); + } + return $types; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\Adapter as Adapter; + +/** + * Transaction Helper + * + * This code was originally part of the facade, however it has + * been decided to remove unique features to service classes like + * this to make them available to developers not using the facade class. + * + * Database transaction helper. This is a convenience class + * to perform a callback in a database transaction. This class + * contains a method to wrap your callback in a transaction. + * + * @file RedBeanPHP/Util/Transaction.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Transaction +{ + /** + * Wraps a transaction around a closure or string callback. + * If an Exception is thrown inside, the operation is automatically rolled back. + * If no Exception happens, it commits automatically. + * It also supports (simulated) nested transactions (that is useful when + * you have many methods that needs transactions but are unaware of + * each other). + * + * Example: + * + * + * $from = 1; + * $to = 2; + * $amount = 300; + * + * R::transaction(function() use($from, $to, $amount) + * { + * $accountFrom = R::load('account', $from); + * $accountTo = R::load('account', $to); + * $accountFrom->money -= $amount; + * $accountTo->money += $amount; + * R::store($accountFrom); + * R::store($accountTo); + * }); + * + * + * @param Adapter $adapter Database Adapter providing transaction mechanisms. + * @param callable $callback Closure (or other callable) with the transaction logic + * + * @return mixed + */ + public static function transaction( Adapter $adapter, $callback ) + { + if ( !is_callable( $callback ) ) { + throw new RedException( 'R::transaction needs a valid callback.' ); + } + + static $depth = 0; + $result = null; + try { + if ( $depth == 0 ) { + $adapter->startTransaction(); + } + $depth++; + $result = call_user_func( $callback ); //maintain 5.2 compatibility + $depth--; + if ( $depth == 0 ) { + $adapter->commit(); + } + } catch ( \Exception $exception ) { + $depth--; + if ( $depth == 0 ) { + $adapter->rollback(); + } + throw $exception; + } + return $result; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\ToolBox as ToolBox; + +/** + * Quick Export Utility + * + * The Quick Export Utility Class provides functionality to easily + * expose the result of SQL queries as well-known formats like CSV. + * + * @file RedBeanPHP/Util/QuickExporft.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class QuickExport +{ + /** + * @var Finder + */ + protected $toolbox; + + /** + * @boolean + */ + private static $test = FALSE; + + /** + * Constructor. + * The Quick Export requires a toolbox. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * Makes csv() testable. + */ + public static function operation( $name, $arg1, $arg2 = TRUE ) { + $out = ''; + switch( $name ) { + case 'test': + self::$test = (boolean) $arg1; + break; + case 'header': + $out = ( self::$test ) ? $arg1 : header( $arg1, $arg2 ); + break; + case 'readfile': + $out = ( self::$test ) ? file_get_contents( $arg1 ) : readfile( $arg1 ); + break; + case 'exit': + $out = ( self::$test ) ? 'exit' : exit(); + break; + } + return $out; + } + + /** + * Exposes the result of the specified SQL query as a CSV file. + * + * Usage: + * + * + * R::csv( 'SELECT + * `name`, + * population + * FROM city + * WHERE region = :region ', + * array( ':region' => 'Denmark' ), + * array( 'city', 'population' ), + * '/tmp/cities.csv' + * ); + * + * + * The command above will select all cities in Denmark + * and create a CSV with columns 'city' and 'population' and + * populate the cells under these column headers with the + * names of the cities and the population numbers respectively. + * + * @param string $sql SQL query to expose result of + * @param array $bindings parameter bindings + * @param array $columns column headers for CSV file + * @param string $path path to save CSV file to + * @param boolean $output TRUE to output CSV directly using readfile + * @param array $options delimiter, quote and escape character respectively + * + * @return void + */ + public function csv( $sql = '', $bindings = array(), $columns = NULL, $path = '/tmp/redexport_%s.csv', $output = TRUE, $options = array(',','"','\\') ) + { + list( $delimiter, $enclosure, $escapeChar ) = $options; + $path = sprintf( $path, date('Ymd_his') ); + $handle = fopen( $path, 'w' ); + if ($columns) if (PHP_VERSION_ID>=505040) fputcsv($handle, $columns, $delimiter, $enclosure, $escapeChar ); else fputcsv($handle, $columns, $delimiter, $enclosure ); + $cursor = $this->toolbox->getDatabaseAdapter()->getCursor( $sql, $bindings ); + while( $row = $cursor->getNextItem() ) { + if (PHP_VERSION_ID>=505040) fputcsv($handle, $row, $delimiter, $enclosure, $escapeChar ); else fputcsv($handle, $row, $delimiter, $enclosure ); + } + fclose($handle); + if ( $output ) { + $file = basename($path); + $out = self::operation('header',"Pragma: public"); + $out .= self::operation('header',"Expires: 0"); + $out .= self::operation('header',"Cache-Control: must-revalidate, post-check=0, pre-check=0"); + $out .= self::operation('header',"Cache-Control: private", FALSE ); + $out .= self::operation('header',"Content-Type: text/csv"); + $out .= self::operation('header',"Content-Disposition: attachment; filename={$file}" ); + $out .= self::operation('header',"Content-Transfer-Encoding: binary"); + $out .= self::operation('readfile',$path ); + @unlink( $path ); + self::operation('exit', FALSE); + return $out; + } + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\Finder; + +/** + * MatchUp Utility + * + * Tired of creating login systems and password-forget systems? + * MatchUp is an ORM-translation of these kind of problems. + * A matchUp is a match-and-update combination in terms of beans. + * Typically login related problems are all about a match and + * a conditional update. + * + * @file RedBeanPHP/Util/MatchUp.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class MatchUp +{ + /** + * @var Toolbox + */ + protected $toolbox; + + /** + * Constructor. + * The MatchUp class requires a toolbox + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * MatchUp is a powerful productivity boosting method that can replace simple control + * scripts with a single RedBeanPHP command. Typically, matchUp() is used to + * replace login scripts, token generation scripts and password reset scripts. + * The MatchUp method takes a bean type, an SQL query snippet (starting at the WHERE clause), + * SQL bindings, a pair of task arrays and a bean reference. + * + * If the first 3 parameters match a bean, the first task list will be considered, + * otherwise the second one will be considered. On consideration, each task list, + * an array of keys and values will be executed. Every key in the task list should + * correspond to a bean property while every value can either be an expression to + * be evaluated or a closure (PHP 5.3+). After applying the task list to the bean + * it will be stored. If no bean has been found, a new bean will be dispensed. + * + * This method will return TRUE if the bean was found and FALSE if not AND + * there was a NOT-FOUND task list. If no bean was found AND there was also + * no second task list, NULL will be returned. + * + * To obtain the bean, pass a variable as the sixth parameter. + * The function will put the matching bean in the specified variable. + * + * Usage (this example resets a password in one go): + * + * + * $newpass = '1234'; + * $didResetPass = R::matchUp( + * 'account', ' token = ? AND tokentime > ? ', + * [ $token, time()-100 ], + * [ 'pass' => $newpass, 'token' => '' ], + * NULL, + * $account ); + * + * + * @param string $type type of bean you're looking for + * @param string $sql SQL snippet (starting at the WHERE clause, omit WHERE-keyword) + * @param array $bindings array of parameter bindings for SQL snippet + * @param array $onFoundDo task list to be considered on finding the bean + * @param array $onNotFoundDo task list to be considered on NOT finding the bean + * @param OODBBean &$bean reference to obtain the found bean + * + * @return mixed + */ + public function matchUp( $type, $sql, $bindings = array(), $onFoundDo = NULL, $onNotFoundDo = NULL, &$bean = NULL ) + { + $finder = new Finder( $this->toolbox ); + $oodb = $this->toolbox->getRedBean(); + $bean = $finder->findOne( $type, $sql, $bindings ); + if ( $bean && $onFoundDo ) { + foreach( $onFoundDo as $property => $value ) { + if ( function_exists('is_callable') && is_callable( $value ) ) { + $bean[$property] = call_user_func_array( $value, array( $bean ) ); + } else { + $bean[$property] = $value; + } + } + $oodb->store( $bean ); + return TRUE; + } + if ( $onNotFoundDo ) { + $bean = $oodb->dispense( $type ); + foreach( $onNotFoundDo as $property => $value ) { + if ( function_exists('is_callable') && is_callable( $value ) ) { + $bean[$property] = call_user_func_array( $value, array( $bean ) ); + } else { + $bean[$property] = $value; + } + } + $oodb->store( $bean ); + return FALSE; + } + return NULL; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\Finder; + +/** + * Look Utility + * + * The Look Utility class provides an easy way to generate + * tables and selects (pulldowns) from the database. + * + * @file RedBeanPHP/Util/Look.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Look +{ + /** + * @var Toolbox + */ + protected $toolbox; + + /** + * Constructor. + * The MatchUp class requires a toolbox + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * Takes an full SQL query with optional bindings, a series of keys, a template + * and optionally a filter function and glue and assembles a view from all this. + * This is the fastest way from SQL to view. Typically this function is used to + * generate pulldown (select tag) menus with options queried from the database. + * + * Usage: + * + * + * $htmlPulldown = R::look( + * 'SELECT * FROM color WHERE value != ? ORDER BY value ASC', + * [ 'g' ], + * [ 'value', 'name' ], + * '', + * 'strtoupper', + * "\n" + * ); + * + * + * The example above creates an HTML fragment like this: + * + * + * + * + * to pick a color from a palette. The HTML fragment gets constructed by + * an SQL query that selects all colors that do not have value 'g' - this + * excludes green. Next, the bean properties 'value' and 'name' are mapped to the + * HTML template string, note that the order here is important. The mapping and + * the HTML template string follow vsprintf-rules. All property values are then + * passed through the specified filter function 'strtoupper' which in this case + * is a native PHP function to convert strings to uppercase characters only. + * Finally the resulting HTML fragment strings are glued together using a + * newline character specified in the last parameter for readability. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * @param string $sql query to execute + * @param array $bindings parameters to bind to slots mentioned in query or an empty array + * @param array $keys names in result collection to map to template + * @param string $template HTML template to fill with values associated with keys, use printf notation (i.e. %s) + * @param callable $filter function to pass values through (for translation for instance) + * @param string $glue optional glue to use when joining resulting strings + * + * @return string + */ + public function look( $sql, $bindings = array(), $keys = array( 'selected', 'id', 'name' ), $template = '', $filter = 'trim', $glue = '' ) + { + $adapter = $this->toolbox->getDatabaseAdapter(); + $lines = array(); + $rows = $adapter->get( $sql, $bindings ); + foreach( $rows as $row ) { + $values = array(); + foreach( $keys as $key ) { + if (!empty($filter)) { + $values[] = call_user_func_array( $filter, array( $row[$key] ) ); + } else { + $values[] = $row[$key]; + } + } + $lines[] = vsprintf( $template, $values ); + } + $string = implode( $glue, $lines ); + return $string; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\Finder; + +/** + * Diff Utility + * + * The Look Utility class provides an easy way to generate + * tables and selects (pulldowns) from the database. + * + * @file RedBeanPHP/Util/Diff.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Diff +{ + /** + * @var Toolbox + */ + protected $toolbox; + + /** + * Constructor. + * The MatchUp class requires a toolbox + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * Calculates a diff between two beans (or arrays of beans). + * The result of this method is an array describing the differences of the second bean compared to + * the first, where the first bean is taken as reference. The array is keyed by type/property, id and property name, where + * type/property is either the type (in case of the root bean) or the property of the parent bean where the type resides. + * The diffs are mainly intended for logging, you cannot apply these diffs as patches to other beans. + * However this functionality might be added in the future. + * + * The keys of the array can be formatted using the $format parameter. + * A key will be composed of a path (1st), id (2nd) and property (3rd). + * Using printf-style notation you can determine the exact format of the key. + * The default format will look like: + * + * 'book.1.title' => array( , ) + * + * If you only want a simple diff of one bean and you don't care about ids, + * you might pass a format like: '%1$s.%3$s' which gives: + * + * 'book.1.title' => array( , ) + * + * The filter parameter can be used to set filters, it should be an array + * of property names that have to be skipped. By default this array is filled with + * two strings: 'created' and 'modified'. + * + * @param OODBBean|array $beans reference beans + * @param OODBBean|array $others beans to compare + * @param array $filters names of properties of all beans to skip + * @param string $format the format of the key, defaults to '%s.%s.%s' + * @param string $type type/property of bean to use for key generation + * + * @return array + */ + public function diff( $beans, $others, $filters = array( 'created', 'modified' ), $format = '%s.%s.%s', $type = NULL ) + { + $diff = array(); + + if ( !is_array( $beans ) ) $beans = array( $beans ); + $beansI = array(); + foreach ( $beans as $bean ) { + if ( !( $bean instanceof OODBBean ) ) continue; + $beansI[$bean->id] = $bean; + } + + if ( !is_array( $others ) ) $others = array( $others ); + $othersI = array(); + foreach ( $others as $other ) { + if ( !( $other instanceof OODBBean ) ) continue; + $othersI[$other->id] = $other; + } + + if ( count( $beansI ) == 0 || count( $othersI ) == 0 ) { + return array(); + } + + $type = $type != NULL ? $type : reset($beansI)->getMeta( 'type' ); + + foreach( $beansI as $id => $bean ) { + if ( !isset( $othersI[$id] ) ) continue; + $other = $othersI[$id]; + foreach( $bean as $property => $value ) { + if ( in_array( $property, $filters ) ) continue; + $key = vsprintf( $format, array( $type, $bean->id, $property ) ); + $compare = $other->{$property}; + if ( !is_object( $value ) && !is_array( $value ) && $value != $compare ) { + $diff[$key] = array( $value, $compare ); + } else { + $diff = array_merge( $diff, $this->diff( $value, $compare, $filters, $format, $key ) ); + } + } + } + + return $diff; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\ToolBox; +use RedBeanPHP\OODBBean; + +/** + * Tree + * + * Given a bean, finds it children or parents + * in a hierchical structure. + * + * @experimental feature + * + * @file RedBeanPHP/Util/Tree.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 Tree { + + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * @var QueryWriter + */ + protected $writer; + + /** + * @var OODB + */ + protected $oodb; + + /** + * Constructor, creates a new instance of + * the Tree. + * + * @param ToolBox $toolbox toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->writer = $toolbox->getWriter(); + $this->oodb = $toolbox->getRedBean(); + } + + /** + * Returns all child beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * Usage: + * + * + * $newsArticles = R::children( $newsPage, ' ORDER BY title ASC ' ) + * $newsArticles = R::children( $newsPage, ' WHERE title = ? ', [ $t ] ); + * $newsArticles = R::children( $newsPage, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @param OODBBean $bean reference bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * + * @return array + */ + public function children( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + $type = $bean->getMeta('type'); + $id = $bean->id; + + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, FALSE, $sql, $bindings ); + + return $this->oodb->convertToBeans( $type, $rows ); + } + + /** + * Returns all parent beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $newsPages = R::parents( $newsArticle, ' ORDER BY title ASC ' ); + * $newsPages = R::parents( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $newsPages = R::parents( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @param OODBBean $bean reference bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * + * @return array + */ + public function parents( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + $type = $bean->getMeta('type'); + $id = $bean->id; + + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, TRUE, $sql, $bindings ); + + return $this->oodb->convertToBeans( $type, $rows ); + } + + /** + * Counts all children beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $count = R::countChildren( $newsArticle ); + * $count = R::countChildren( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $count = R::countChildren( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no SQL or select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean reference bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + * + * @return integer + */ + public function countChildren( OODBBean $bean, $sql = NULL, $bindings = array(), $select = TRUE ) { + $type = $bean->getMeta('type'); + $id = $bean->id; + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, FALSE, $sql, $bindings, $select ); + $first = reset($rows); + $cell = reset($first); + return (intval($cell) - (($select === TRUE && is_null($sql)) ? 1 : 0)); + } + + /** + * Counts all parent beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $count = R::countParents( $newsArticle ); + * $count = R::countParents( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $count = R::countParents( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * Note: + * By default, if no SQL or select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean reference bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + * + * @return integer + */ + public function countParents( OODBBean $bean, $sql = NULL, $bindings = array(), $select = TRUE ) { + $type = $bean->getMeta('type'); + $id = $bean->id; + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, TRUE, $sql, $bindings, $select ); + $first = reset($rows); + $cell = reset($first); + return (intval($cell) - (($select === TRUE && is_null($sql)) ? 1 : 0)); + } +} +} + +namespace RedBeanPHP\Util { +use RedBeanPHP\Facade as R; +use RedBeanPHP\OODBBean; + +/** + * Feature Utility + * + * The Feature Utility class provides an easy way to turn + * on or off features. This allows us to introduce new features + * without accidentally breaking backward compatibility. + * + * @file RedBeanPHP/Util/Feature.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 Feature +{ + /* Feature set constants */ + const C_FEATURE_NOVICE_LATEST = 'novice/latest'; + const C_FEATURE_LATEST = 'latest'; + const C_FEATURE_NOVICE_5_5 = 'novice/5.5'; + const C_FEATURE_5_5 = '5.5'; + const C_FEATURE_NOVICE_5_4 = 'novice/5.4'; + const C_FEATURE_5_4 = '5.4'; + const C_FEATURE_NOVICE_5_3 = 'novice/5.3'; + const C_FEATURE_5_3 = '5.3'; + const C_FEATURE_ORIGINAL = 'original'; + + /** + * Selects the feature set you want as specified by + * the label. + * + * Available labels: + * + * novice/latest: + * - forbid R::nuke() + * - enable automatic relation resolver based on foreign keys + * - forbid R::store(All)( $bean, TRUE ) (Hybrid mode) + * - use IS-NULL conditions in findLike() etc + * + * latest: + * - allow R::nuke() + * - enable auto resolve + * - allow hybrid mode + * - use IS-NULL conditions in findLike() etc + * + * novice/X or X: + * - keep everything as it was in version X + * + * Usage: + * + * + * R::useFeatureSet( 'novice/latest' ); + * + * + * @param string $label label + * + * @return void + */ + public static function feature( $label ) { + switch( $label ) { + case self::C_FEATURE_NOVICE_LATEST: + case self::C_FEATURE_NOVICE_5_4: + case self::C_FEATURE_NOVICE_5_5: + OODBBean::useFluidCount( FALSE ); + R::noNuke( TRUE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( TRUE ); + break; + case self::C_FEATURE_LATEST: + case self::C_FEATURE_5_4: + case self::C_FEATURE_5_5: + OODBBean::useFluidCount( FALSE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( TRUE ); + R::useISNULLConditions( TRUE ); + break; + case self::C_FEATURE_NOVICE_5_3: + OODBBean::useFluidCount( TRUE ); + R::noNuke( TRUE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + case self::C_FEATURE_5_3: + OODBBean::useFluidCount( TRUE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + case self::C_FEATURE_ORIGINAL: + OODBBean::useFluidCount( TRUE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + default: + throw new \Exception("Unknown feature set label."); + break; + } + } +} +} + +namespace RedBeanPHP { + +/** + * RedBean Plugin. + * Marker interface for plugins. + * Use this interface when defining new plugins, it's an + * easy way for the rest of the application to recognize your + * plugin. This plugin interface does not require you to + * implement a specific API. + * + * @file RedBean/Plugin.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Plugin +{ +} + +; +} +namespace { + +//make some classes available for backward compatibility +class RedBean_SimpleModel extends \RedBeanPHP\SimpleModel {}; + +if (!class_exists('R')) { + class R extends \RedBeanPHP\Facade{}; +} + + + +/** + * Support functions for RedBeanPHP. + * Additional convenience shortcut functions for RedBeanPHP. + * + * @file RedBeanPHP/Functions.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ + +/** + * Convenience function for ENUM short syntax in queries. + * + * Usage: + * + * + * R::find( 'paint', ' color_id = ? ', [ EID('color:yellow') ] ); + * + * + * If a function called EID() already exists you'll have to write this + * wrapper yourself ;) + * + * @param string $enumName enum code as you would pass to R::enum() + * + * @return mixed + */ +if (!function_exists('EID')) { + + function EID($enumName) + { + return \RedBeanPHP\Facade::enum( $enumName )->id; + } + +} + +/** + * Prints the result of R::dump() to the screen using + * print_r. + * + * @param mixed $data data to dump + * + * @return void + */ +if ( !function_exists( 'dmp' ) ) { + + function dmp( $list ) + { + print_r( \RedBeanPHP\Facade::dump( $list ) ); + } +} + +/** + * Function alias for R::genSlots(). + */ +if ( !function_exists( 'genslots' ) ) { + + function genslots( $slots, $tpl = NULL ) + { + return \RedBeanPHP\Facade::genSlots( $slots, $tpl ); + } +} + +/** + * Function alias for R::flat(). + */ +if ( !function_exists( 'array_flatten' ) ) { + + function array_flatten( $array ) + { + return \RedBeanPHP\Facade::flat( $array ); + } +} + + +}