PageRenderTime 57ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/com/db.php

http://github.com/unirgy/buckyball
PHP | 2641 lines | 1566 code | 196 blank | 879 comment | 266 complexity | bac9ee45ef4368a2896f41023adeb8cb MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /**
  3. * Copyright 2011 Unirgy LLC
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. * @package BuckyBall
  18. * @link http://github.com/unirgy/buckyball
  19. * @author Boris Gurvich <boris@unirgy.com>
  20. * @copyright (c) 2010-2012 Boris Gurvich
  21. * @license http://www.apache.org/licenses/LICENSE-2.0.html
  22. */
  23. /**
  24. * Wrapper for idiorm/paris
  25. *
  26. * @see http://j4mie.github.com/idiormandparis/
  27. */
  28. class BDb
  29. {
  30. /**
  31. * Collection of cached named DB connections
  32. *
  33. * @var array
  34. */
  35. protected static $_namedConnections = array();
  36. /**
  37. * Necessary configuration for each DB connection name
  38. *
  39. * @var array
  40. */
  41. protected static $_namedConnectionConfig = array();
  42. /**
  43. * Default DB connection name
  44. *
  45. * @var string
  46. */
  47. protected static $_defaultConnectionName = 'DEFAULT';
  48. /**
  49. * DB name which is currently referenced in BORM::$_db
  50. *
  51. * @var string
  52. */
  53. protected static $_currentConnectionName;
  54. /**
  55. * Current DB configuration
  56. *
  57. * @var array
  58. */
  59. protected static $_config = array('table_prefix'=>'');
  60. /**
  61. * List of tables per connection
  62. *
  63. * @var array
  64. */
  65. protected static $_tables = array();
  66. /**
  67. * Shortcut to help with IDE autocompletion
  68. * @param bool $new
  69. * @param array $args
  70. * @return BDb
  71. */
  72. public static function i($new=false, array $args=array())
  73. {
  74. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  75. }
  76. /**
  77. * Connect to DB using default or a named connection from global configuration
  78. *
  79. * Connections are cached for reuse when switching.
  80. *
  81. * Structure in configuration:
  82. *
  83. * {
  84. * db: {
  85. * dsn: 'mysql:host=127.0.0.1;dbname=buckyball', - optional: replaces engine, host, dbname
  86. * engine: 'mysql', - optional if dsn exists, default: mysql
  87. * host: '127.0.0.1', - optional if dsn exists, default: 127.0.0.1
  88. * dbname: 'buckyball', - optional if dsn exists, required otherwise
  89. * username: 'dbuser', - default: root
  90. * password: 'password', - default: (empty)
  91. * logging: false, - default: false
  92. * named: {
  93. * read: {<db-connection-structure>}, - same structure as default connection
  94. * write: {
  95. * use: 'read' - optional, reuse another connection
  96. * }
  97. * }
  98. * }
  99. *
  100. * @param string $name
  101. * @throws BException
  102. * @return PDO
  103. */
  104. public static function connect($name=null)
  105. {
  106. if (!$name && static::$_currentConnectionName) { // continue connection to current db, if no value
  107. return BORM::get_db();
  108. }
  109. if (is_null($name)) { // if first time connection, connect to default db
  110. $name = static::$_defaultConnectionName;
  111. }
  112. if ($name===static::$_currentConnectionName) { // if currently connected to requested db, return
  113. return BORM::get_db();
  114. }
  115. if (!empty(static::$_namedConnections[$name])) { // if connection already exists, switch to it
  116. BDebug::debug('DB.SWITCH '.$name);
  117. static::$_currentConnectionName = $name;
  118. static::$_config = static::$_namedConnectionConfig[$name];
  119. BORM::set_db(static::$_namedConnections[$name], static::$_config);
  120. return BORM::get_db();
  121. }
  122. $config = BConfig::i()->get($name===static::$_defaultConnectionName ? 'db' : 'db/named/'.$name);
  123. if (!$config) {
  124. throw new BException(BLocale::_('Invalid or missing DB configuration: %s', $name));
  125. }
  126. if (!empty($config['use'])) { //TODO: Prevent circular reference
  127. static::connect($config['use']);
  128. return BORM::get_db();
  129. }
  130. if (!empty($config['dsn'])) {
  131. $dsn = $config['dsn'];
  132. if (empty($config['dbname']) && preg_match('#dbname=(.*?)(;|$)#', $dsn, $m)) {
  133. $config['dbname'] = $m[1];
  134. }
  135. } else {
  136. if (empty($config['dbname'])) {
  137. throw new BException(BLocale::_("dbname configuration value is required for '%s'", $name));
  138. }
  139. $engine = !empty($config['engine']) ? $config['engine'] : 'mysql';
  140. $host = !empty($config['host']) ? $config['host'] : '127.0.0.1';
  141. switch ($engine) {
  142. case "mysql":
  143. $dsn = "mysql:host={$host};dbname={$config['dbname']};charset=UTF8";
  144. break;
  145. default:
  146. throw new BException(BLocale::_('Invalid DB engine: %s', $engine));
  147. }
  148. }
  149. $profile = BDebug::debug('DB.CONNECT '.$name);
  150. static::$_currentConnectionName = $name;
  151. BORM::configure($dsn);
  152. BORM::configure('username', !empty($config['username']) ? $config['username'] : 'root');
  153. BORM::configure('password', !empty($config['password']) ? $config['password'] : '');
  154. BORM::configure('logging', !empty($config['logging']));
  155. BORM::set_db(null);
  156. BORM::setup_db();
  157. static::$_namedConnections[$name] = BORM::get_db();
  158. static::$_config = static::$_namedConnectionConfig[$name] = array(
  159. 'dbname' => !empty($config['dbname']) ? $config['dbname'] : null,
  160. 'table_prefix' => !empty($config['table_prefix']) ? $config['table_prefix'] : '',
  161. );
  162. $db = BORM::get_db();
  163. BDebug::profile($profile);
  164. return $db;
  165. }
  166. /**
  167. * DB friendly current date/time
  168. *
  169. * @return string
  170. */
  171. public static function now()
  172. {
  173. return gmstrftime('%Y-%m-%d %H:%M:%S');
  174. }
  175. /**
  176. * Shortcut to run multiple queries from migrate scripts
  177. *
  178. * It doesn't make sense to run multiple queries in the same call and use $params
  179. *
  180. * @param string $sql
  181. * @param array $params
  182. * @param array $options
  183. * - echo - echo all queries as they run
  184. * @throws Exception
  185. * @return array
  186. */
  187. public static function run($sql, $params=null, $options=array())
  188. {
  189. BDb::connect();
  190. $queries = preg_split("/;+(?=([^'|^\\\']*['|\\\'][^'|^\\\']*['|\\\'])*[^'|^\\\']*[^'|^\\\']$)/", $sql);
  191. $results = array();
  192. foreach ($queries as $i=>$query){
  193. if (strlen(trim($query)) > 0) {
  194. try {
  195. BDebug::debug('DB.RUN: '.$query);
  196. if (!empty($options['echo'])) {
  197. echo '<hr><pre>'.$query.'<pre>';
  198. }
  199. if (is_null($params)) {
  200. $results[] = BORM::get_db()->exec($query);
  201. } else {
  202. $results[] = BORM::get_db()->prepare($query)->execute($params);
  203. }
  204. } catch (Exception $e) {
  205. echo "<hr>{$e->getMessage()}: <pre>{$query}</pre>";
  206. if (empty($options['try'])) {
  207. throw $e;
  208. }
  209. }
  210. }
  211. }
  212. return $results;
  213. }
  214. /**
  215. * Start transaction
  216. *
  217. * @param string $connectionName
  218. */
  219. public static function transaction($connectionName=null)
  220. {
  221. if (!is_null($connectionName)) {
  222. BDb::connect($connectionName);
  223. }
  224. BORM::get_db()->beginTransaction();
  225. }
  226. /**
  227. * Commit transaction
  228. *
  229. * @param string $connectionName
  230. */
  231. public static function commit($connectionName=null)
  232. {
  233. if (!is_null($connectionName)) {
  234. BDb::connect($connectionName);
  235. }
  236. BORM::get_db()->commit();
  237. }
  238. /**
  239. * Rollback transaction
  240. *
  241. * @param string $connectionName
  242. */
  243. public static function rollback($connectionName=null)
  244. {
  245. if (!is_null($connectionName)) {
  246. BDb::connect($connectionName);
  247. }
  248. BORM::get_db()->rollback();
  249. }
  250. /**
  251. * Get db specific table name with pre-configured prefix for current connection
  252. *
  253. * Can be used as both BDb::t() and $this->t() within migration script
  254. * Convenient within strings and heredocs as {$this->t(...)}
  255. *
  256. * @param string $tableName
  257. * @return string
  258. */
  259. public static function t($tableName)
  260. {
  261. $a = explode('.', $tableName);
  262. $p = static::$_config['table_prefix'];
  263. return !empty($a[1]) ? $a[0].'.'.$p.$a[1] : $p.$a[0];
  264. }
  265. /**
  266. * Convert array collection of objects from find_many result to arrays
  267. *
  268. * @param array $rows result of ORM::find_many()
  269. * @param string $method default 'as_array'
  270. * @param array|string $fields if specified, return only these fields
  271. * @param boolean $maskInverse if true, do not return specified fields
  272. * @return array
  273. */
  274. public static function many_as_array($rows, $method='as_array', $fields=null, $maskInverse=false)
  275. {
  276. $res = array();
  277. foreach ((array)$rows as $i=>$r) {
  278. if (!$r instanceof BModel) {
  279. echo "Rows are not models: <pre>"; print_r($r);
  280. debug_print_backtrace();
  281. exit;
  282. }
  283. $row = $r->$method();
  284. if (!is_null($fields)) $row = BUtil::arrayMask($row, $fields, $maskInverse);
  285. $res[$i] = $row;
  286. }
  287. return $res;
  288. }
  289. /**
  290. * Construct where statement (for delete or update)
  291. *
  292. * Examples:
  293. * $w = BDb::where("f1 is null");
  294. *
  295. * // (f1='V1') AND (f2='V2')
  296. * $w = BDb::where(array('f1'=>'V1', 'f2'=>'V2'));
  297. *
  298. * // (f1=5) AND (f2 LIKE '%text%'):
  299. * $w = BDb::where(array('f1'=>5, array('f2 LIKE ?', '%text%')));
  300. *
  301. * // ((f1!=5) OR (f2 BETWEEN 10 AND 20)):
  302. * $w = BDb::where(array('OR'=>array(array('f1!=?', 5), array('f2 BETWEEN ? AND ?', 10, 20))));
  303. *
  304. * // (f1 IN (1,2,3)) AND NOT ((f2 IS NULL) OR (f2=10))
  305. * $w = BDb::where(array('f1'=>array(1,2,3)), 'NOT'=>array('OR'=>array("f2 IS NULL", 'f2'=>10)));
  306. *
  307. * // ((A OR B) AND (C OR D))
  308. * $w = BDb::where(array('AND', array('OR', 'A', 'B'), array('OR', 'C', 'D')));
  309. *
  310. * @param array $conds
  311. * @param boolean $or
  312. * @throws BException
  313. * @return array (query, params)
  314. */
  315. public static function where($conds, $or=false)
  316. {
  317. if (is_string($conds)) {
  318. return array($conds, array());
  319. }
  320. if (!is_array($conds)) {
  321. throw new BException("Invalid where parameter");
  322. }
  323. $where = array();
  324. $params = array();
  325. foreach ($conds as $f=>$v) {
  326. if (is_int($f)) {
  327. if (is_string($v)) { // freeform
  328. $where[] = '('.$v.')';
  329. continue;
  330. }
  331. if (is_array($v)) { // [freeform|arguments]
  332. $sql = array_shift($v);
  333. if ('AND'===$sql || 'OR'===$sql || 'NOT'===$sql) {
  334. $f = $sql;
  335. } else {
  336. if (isset($v[0]) && is_array($v[0])) { // `field` IN (?)
  337. $v = $v[0];
  338. $sql = str_replace('(?)', '('.str_pad('', sizeof($v)*2-1, '?,').')', $sql);
  339. }
  340. $where[] = '('.$sql.')';
  341. $params = array_merge($params, $v);
  342. continue;
  343. }
  344. } else {
  345. throw new BException('Invalid token: '.print_r($v,1));
  346. }
  347. }
  348. if ('AND'===$f) {
  349. list($w, $p) = static::where($v);
  350. $where[] = '('.$w.')';
  351. $params = array_merge($params, $p);
  352. } elseif ('OR'===$f) {
  353. list($w, $p) = static::where($v, true);
  354. $where[] = '('.$w.')';
  355. $params = array_merge($params, $p);
  356. } elseif ('NOT'===$f) {
  357. list($w, $p) = static::where($v);
  358. $where[] = 'NOT ('.$w.')';
  359. $params = array_merge($params, $p);
  360. } elseif (is_array($v)) {
  361. $where[] = "({$f} IN (".str_pad('', sizeof($v)*2-1, '?,')."))";
  362. $params = array_merge($params, $v);
  363. } elseif (is_null($v)) {
  364. $where[] = "({$f} IS NULL)";
  365. } else {
  366. $where[] = "({$f}=?)";
  367. $params[] = $v;
  368. }
  369. }
  370. #print_r($where); print_r($params);
  371. return array(join($or ? " OR " : " AND ", $where), $params);
  372. }
  373. /**
  374. * Get database name for current connection
  375. *
  376. */
  377. public static function dbName()
  378. {
  379. if (!static::$_config) {
  380. throw new BException('No connection selected');
  381. }
  382. return !empty(static::$_config['dbname']) ? static::$_config['dbname'] : null;
  383. }
  384. public static function ddlStart()
  385. {
  386. BDb::run(<<<EOT
  387. /*!40101 SET SQL_MODE=''*/;
  388. /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
  389. /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
  390. /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
  391. /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
  392. EOT
  393. );
  394. }
  395. public static function ddlFinish()
  396. {
  397. BDb::run(<<<EOT
  398. /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
  399. /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
  400. /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
  401. /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
  402. EOT
  403. );
  404. }
  405. /**
  406. * Clear DDL cache
  407. *
  408. */
  409. public static function ddlClearCache($fullTableName=null)
  410. {
  411. if ($fullTableName) {
  412. if (!static::dbName()) {
  413. static::connect(static::$_defaultConnectionName);
  414. }
  415. $a = explode('.', $fullTableName);
  416. $dbName = empty($a[1]) ? static::dbName() : $a[0];
  417. $tableName = empty($a[1]) ? $fullTableName : $a[1];
  418. static::$_tables[$dbName][$tableName] = null;
  419. } else {
  420. static::$_tables = array();
  421. }
  422. }
  423. /**
  424. * Check whether table exists
  425. *
  426. * @param string $fullTableName
  427. * @return bool
  428. */
  429. public static function ddlTableExists($fullTableName)
  430. {
  431. if (!static::dbName()) {
  432. static::connect(static::$_defaultConnectionName);
  433. }
  434. $a = explode('.', $fullTableName);
  435. $dbName = empty($a[1]) ? static::dbName() : $a[0];
  436. $tableName = empty($a[1]) ? $fullTableName : $a[1];
  437. if (!isset(static::$_tables[$dbName])) {
  438. $tables = BORM::i()->raw_query("SHOW TABLES FROM `{$dbName}`", array())->find_many();
  439. $field = "Tables_in_{$dbName}";
  440. foreach ($tables as $t) {
  441. static::$_tables[$dbName][$t->get($field)] = array();
  442. }
  443. } elseif (!isset(static::$_tables[$dbName][$tableName])) {
  444. $table = BORM::i()->raw_query("SHOW TABLES FROM `{$dbName}` LIKE ?", array($tableName))->find_one();
  445. if ($table) {
  446. static::$_tables[$dbName][$tableName] = array();
  447. }
  448. }
  449. return isset(static::$_tables[$dbName][$tableName]);
  450. }
  451. /**
  452. * Get table field info
  453. *
  454. * @param string $fullTableName
  455. * @param string $fieldName if null return all fields
  456. * @throws BException
  457. * @return mixed
  458. */
  459. public static function ddlFieldInfo($fullTableName, $fieldName=null)
  460. {
  461. self::checkTable($fullTableName);
  462. $a = explode('.', $fullTableName);
  463. $dbName = empty($a[1]) ? static::dbName() : $a[0];
  464. $tableName = empty($a[1]) ? $fullTableName : $a[1];
  465. if (!isset(static::$_tables[$dbName][$tableName]['fields'])) {
  466. static::$_tables[$dbName][$tableName]['fields'] = BORM::i()
  467. ->raw_query("SHOW FIELDS FROM `{$dbName}`.`{$tableName}`", array())->find_many_assoc('Field');
  468. }
  469. $res = static::$_tables[$dbName][$tableName]['fields'];
  470. return is_null($fieldName) ? $res : (isset($res[$fieldName]) ? $res[$fieldName] : null);
  471. }
  472. /**
  473. * @param string $fullTableName
  474. * @throws BException
  475. */
  476. protected static function checkTable($fullTableName)
  477. {
  478. if (!static::ddlTableExists($fullTableName)) {
  479. throw new BException(BLocale::_('Invalid table name: %s', $fullTableName));
  480. }
  481. }
  482. /**
  483. * Retrieve table index(es) info, if exist
  484. *
  485. * @param string $fullTableName
  486. * @param string $indexName
  487. * @throws BException
  488. * @return array|null
  489. */
  490. public static function ddlIndexInfo($fullTableName, $indexName=null)
  491. {
  492. if (!static::ddlTableExists($fullTableName)) {
  493. throw new BException(BLocale::_('Invalid table name: %s', $fullTableName));
  494. }
  495. $a = explode('.', $fullTableName);
  496. $dbName = empty($a[1]) ? static::dbName() : $a[0];
  497. $tableName = empty($a[1]) ? $fullTableName : $a[1];
  498. if (!isset(static::$_tables[$dbName][$tableName]['indexes'])) {
  499. static::$_tables[$dbName][$tableName]['indexes'] = BORM::i()
  500. ->raw_query("SHOW KEYS FROM `{$dbName}`.`{$tableName}`", array())->find_many_assoc('Key_name');
  501. }
  502. $res = static::$_tables[$dbName][$tableName]['indexes'];
  503. return is_null($indexName) ? $res : (isset($res[$indexName]) ? $res[$indexName] : null);
  504. }
  505. /**
  506. * Retrieve table foreign key(s) info, if exist
  507. *
  508. * Mysql/InnoDB specific
  509. *
  510. * @param string $fullTableName
  511. * @param string $fkName
  512. * @throws BException
  513. * @return array|null
  514. */
  515. public static function ddlForeignKeyInfo($fullTableName, $fkName=null)
  516. {
  517. if (!static::ddlTableExists($fullTableName)) {
  518. throw new BException(BLocale::_('Invalid table name: %s', $fullTableName));
  519. }
  520. $a = explode('.', $fullTableName);
  521. $dbName = empty($a[1]) ? static::dbName() : $a[0];
  522. $tableName = empty($a[1]) ? $fullTableName : $a[1];
  523. if (!isset(static::$_tables[$dbName][$tableName]['fks'])) {
  524. static::$_tables[$dbName][$tableName]['fks'] = BORM::i()
  525. ->raw_query("SELECT * FROM information_schema.TABLE_CONSTRAINTS
  526. WHERE TABLE_SCHEMA='{$dbName}' AND TABLE_NAME='{$tableName}'
  527. AND CONSTRAINT_TYPE='FOREIGN KEY'", array())->find_many_assoc('CONSTRAINT_NAME');
  528. }
  529. $res = static::$_tables[$dbName][$tableName]['fks'];
  530. return is_null($fkName) ? $res : (isset($res[$fkName]) ? $res[$fkName] : null);
  531. }
  532. /**
  533. * Create or update table
  534. *
  535. * @deprecates ddlTable and ddlTableColumns
  536. * @param string $fullTableName
  537. * @param array $def
  538. * @throws BException
  539. * @return array
  540. */
  541. public static function ddlTableDef($fullTableName, $def)
  542. {
  543. $fields = !empty($def['COLUMNS']) ? $def['COLUMNS'] : null;
  544. $primary = !empty($def['PRIMARY']) ? $def['PRIMARY'] : null;
  545. $indexes = !empty($def['KEYS']) ? $def['KEYS'] : null;
  546. $fks = !empty($def['CONSTRAINTS']) ? $def['CONSTRAINTS'] : null;
  547. $options = !empty($def['OPTIONS']) ? $def['OPTIONS'] : null;
  548. if (!static::ddlTableExists($fullTableName)) {
  549. if (!$fields) {
  550. throw new BException('Missing fields definition for new table');
  551. }
  552. // temporary code duplication with ddlTable, until the other one is removed
  553. $fieldsArr = array();
  554. foreach ($fields as $f=>$def) {
  555. $fieldsArr[] = '`'.$f.'` '.$def;
  556. }
  557. $fields = null; // reset before update step
  558. if ($primary) {
  559. $fieldsArr[] = "PRIMARY KEY ".$primary;
  560. $primary = null; // reset before update step
  561. }
  562. $engine = !empty($options['engine']) ? $options['engine'] : 'InnoDB';
  563. $charset = !empty($options['charset']) ? $options['charset'] : 'utf8';
  564. $collate = !empty($options['collate']) ? $options['collate'] : 'utf8_general_ci';
  565. BORM::i()->raw_query("CREATE TABLE {$fullTableName} (".join(', ', $fieldsArr).")
  566. ENGINE={$engine} DEFAULT CHARSET={$charset} COLLATE={$collate}", array())->execute();
  567. }
  568. static::ddlTableColumns($fullTableName, $fields, $indexes, $fks, $options);
  569. static::ddlClearCache();
  570. }
  571. /**
  572. * Create or update table
  573. *
  574. * @param string $fullTableName
  575. * @param array $fields
  576. * @param array $options
  577. * - engine (default InnoDB)
  578. * - charset (default utf8)
  579. * - collate (default utf8_general_ci)
  580. * @return bool
  581. */
  582. public static function ddlTable($fullTableName, $fields, $options=null)
  583. {
  584. if (static::ddlTableExists($fullTableName)) {
  585. static::ddlTableColumns($fullTableName, $fields, null, null, $options); // altering options is not implemented
  586. } else {
  587. $fieldsArr = array();
  588. foreach ($fields as $f=>$def) {
  589. $fieldsArr[] = '`'.$f.'` '.$def;
  590. }
  591. if (!empty($options['primary'])) {
  592. $fieldsArr[] = "PRIMARY KEY ".$options['primary'];
  593. }
  594. $engine = !empty($options['engine']) ? $options['engine'] : 'InnoDB';
  595. $charset = !empty($options['charset']) ? $options['charset'] : 'utf8';
  596. $collate = !empty($options['collate']) ? $options['collate'] : 'utf8_general_ci';
  597. BORM::i()->raw_query("CREATE TABLE {$fullTableName} (".join(', ', $fieldsArr).")
  598. ENGINE={$engine} DEFAULT CHARSET={$charset} COLLATE={$collate}", array())->execute();
  599. static::ddlClearCache();
  600. }
  601. return true;
  602. }
  603. /**
  604. * Add or change table columns
  605. *
  606. * BDb::ddlTableColumns('my_table', array(
  607. * 'field_to_create' => 'varchar(255) not null',
  608. * 'field_to_update' => 'decimal(12,2) null',
  609. * 'field_to_drop' => 'DROP',
  610. * ));
  611. *
  612. * @param string $fullTableName
  613. * @param array $fields
  614. * @param array $indexes
  615. * @param array $fks
  616. * @return array
  617. */
  618. public static function ddlTableColumns($fullTableName, $fields, $indexes=null, $fks=null)
  619. {
  620. $tableFields = static::ddlFieldInfo($fullTableName, null);
  621. $tableFields = array_change_key_case($tableFields, CASE_LOWER);
  622. $alterArr = array();
  623. if ($fields) {
  624. foreach ($fields as $f=>$def) {
  625. $fLower = strtolower($f);
  626. if ($def==='DROP') {
  627. if (!empty($tableFields[$fLower])) {
  628. $alterArr[] = "DROP `{$f}`";
  629. }
  630. } elseif (strpos($def, 'RENAME')===0) {
  631. $a = explode(' ', $def, 3); //TODO: smarter parser, allow spaces in column name??
  632. // Why not use a sprintf($def, $f) to fill in column name from $f?
  633. $colName = $a[1];
  634. $def = $a[2];
  635. if (empty($tableFields[$fLower])) {
  636. $f = $colName;
  637. }
  638. $alterArr[] = "CHANGE `{$f}` `{$colName}` {$def}";
  639. } elseif (empty($tableFields[$fLower])) {
  640. $alterArr[] = "ADD `{$f}` {$def}";
  641. } else {
  642. $alterArr[] = "CHANGE `{$f}` `{$f}` {$def}";
  643. }
  644. }
  645. }
  646. if ($indexes) {
  647. $tableIndexes = static::ddlIndexInfo($fullTableName);
  648. $tableIndexes = array_change_key_case($tableIndexes, CASE_LOWER);
  649. foreach ($indexes as $idx=>$def) {
  650. $idxLower = strtolower($idx);
  651. if ($def==='DROP') {
  652. if (!empty($tableIndexes[$idxLower])) {
  653. $alterArr[] = "DROP KEY `{$idx}`";
  654. }
  655. } else {
  656. if (!empty($tableIndexes[$idxLower])) {
  657. $alterArr[] = "DROP KEY `{$idx}`";
  658. }
  659. if (strpos($def, 'PRIMARY')===0) {
  660. $alterArr[] = "DROP PRIMARY KEY";
  661. $def = substr($def, 7);
  662. $alterArr[] = "ADD PRIMARY KEY `{$idx}` {$def}";
  663. } elseif (strpos($def, 'UNIQUE')===0) {
  664. $def = substr($def, 6);
  665. $alterArr[] = "ADD UNIQUE KEY `{$idx}` {$def}";
  666. } else {
  667. $alterArr[] = "ADD KEY `{$idx}` {$def}";
  668. }
  669. }
  670. }
  671. }
  672. if ($fks) {
  673. $tableFKs = static::ddlForeignKeyInfo($fullTableName);
  674. $tableFKs = array_change_key_case($tableFKs, CASE_LOWER);
  675. // @see http://dev.mysql.com/doc/refman/5.5/en/innodb-foreign-key-constraints.html
  676. // You cannot add a foreign key and drop a foreign key in separate clauses of a single ALTER TABLE statement.
  677. // Separate statements are required.
  678. $dropArr = array();
  679. foreach ($fks as $idx=>$def) {
  680. $idxLower = strtolower($idx);
  681. if ($def==='DROP') {
  682. if (!empty($tableFKs[$idxLower])) {
  683. $dropArr[] = "DROP FOREIGN KEY `{$idx}`";
  684. }
  685. } else {
  686. if (!empty($tableFKs[$idxLower])) {
  687. // what if it is not foreign key constraint we do not doe anything to check for UNIQUE and PRIMARY constraint
  688. $dropArr[] = "DROP FOREIGN KEY `{$idx}`";
  689. }
  690. $alterArr[] = "ADD CONSTRAINT `{$idx}` {$def}";
  691. }
  692. }
  693. if (!empty($dropArr)) {
  694. BORM::i()->raw_query("ALTER TABLE {$fullTableName} ".join(", ", $dropArr), array())->execute();
  695. static::ddlClearCache();
  696. }
  697. }
  698. $result = null;
  699. if ($alterArr) {
  700. $result = BORM::i()->raw_query("ALTER TABLE {$fullTableName} ".join(", ", $alterArr), array())->execute();
  701. static::ddlClearCache();
  702. }
  703. return $result;
  704. }
  705. /**
  706. * A convenience method to add columns to table
  707. * It should check if columns exist before passing to self::ddlTableColumns
  708. * $columns array should be in same format as for ddlTableColumns:
  709. *
  710. * array(
  711. * 'field_name' => 'column definition',
  712. * 'field_two' => 'column definition',
  713. * 'field_three' => 'column definition',
  714. * )
  715. *
  716. * @param string $table
  717. * @param array $columns
  718. * @return array|null
  719. */
  720. public static function ddlAddColumns($table, $columns = array())
  721. {
  722. if (empty($columns)) {
  723. BDebug::log(__METHOD__ . ": columns array is empty.");
  724. return null;
  725. }
  726. $pass = array();
  727. $tableFields = array_keys(static::ddlFieldInfo($table));
  728. foreach ($columns as $field => $def) {
  729. if( in_array($field, $tableFields)) {
  730. continue;
  731. }
  732. $pass[$field] = $def;
  733. }
  734. return static::ddlTableColumns($table, $pass);
  735. }
  736. /**
  737. * Clean array or object fields based on table columns and return an array
  738. *
  739. * @param string $table
  740. * @param array|object $data
  741. * @return array
  742. */
  743. public static function cleanForTable($table, $data)
  744. {
  745. $isObject = is_object($data);
  746. $result = array();
  747. foreach ($data as $k=>$v) {
  748. if (BDb::ddlFieldInfo($table, $k)) {
  749. $result[$k] = $isObject ? $data->get($k) : $data[$k];
  750. }
  751. }
  752. return $result;
  753. }
  754. }
  755. /**
  756. * Enhanced PDO class to allow for transaction nesting for mysql and postgresql
  757. *
  758. * @see http://us.php.net/manual/en/pdo.connections.php#94100
  759. * @see http://www.kennynet.co.uk/2008/12/02/php-pdo-nested-transactions/
  760. */
  761. class BPDO extends PDO
  762. {
  763. // Database drivers that support SAVEPOINTs.
  764. protected static $_savepointTransactions = array("pgsql", "mysql");
  765. // The current transaction level.
  766. protected $_transLevel = 0;
  767. /*
  768. public static function exception_handler($exception)
  769. {
  770. // Output the exception details
  771. die('Uncaught exception: '. $exception->getMessage());
  772. }
  773. public function __construct($dsn, $username='', $password='', $driver_options=array())
  774. {
  775. // Temporarily change the PHP exception handler while we . . .
  776. set_exception_handler(array(__CLASS__, 'exception_handler'));
  777. // . . . create a PDO object
  778. parent::__construct($dsn, $username, $password, $driver_options);
  779. // Change the exception handler back to whatever it was before
  780. restore_exception_handler();
  781. }
  782. */
  783. protected function _nestable() {
  784. return in_array($this->getAttribute(PDO::ATTR_DRIVER_NAME),
  785. static::$_savepointTransactions);
  786. }
  787. public function beginTransaction() {
  788. if (!$this->_nestable() || $this->_transLevel == 0) {
  789. parent::beginTransaction();
  790. } else {
  791. $this->exec("SAVEPOINT LEVEL{$this->_transLevel}");
  792. }
  793. $this->_transLevel++;
  794. }
  795. public function commit() {
  796. $this->_transLevel--;
  797. if (!$this->_nestable() || $this->_transLevel == 0) {
  798. parent::commit();
  799. } else {
  800. $this->exec("RELEASE SAVEPOINT LEVEL{$this->_transLevel}");
  801. }
  802. }
  803. public function rollBack() {
  804. $this->_transLevel--;
  805. if (!$this->_nestable() || $this->_transLevel == 0) {
  806. parent::rollBack();
  807. } else {
  808. $this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->_transLevel}");
  809. }
  810. }
  811. }
  812. /**
  813. * Enhanced ORMWrapper to support multiple database connections and many other goodies
  814. */
  815. class BORM extends ORMWrapper
  816. {
  817. /**
  818. * Singleton instance
  819. *
  820. * @var BORM
  821. */
  822. protected static $_instance;
  823. /**
  824. * ID for profiling of the last run query
  825. *
  826. * @var int
  827. */
  828. protected static $_last_profile;
  829. /**
  830. * Default class name for direct ORM calls
  831. *
  832. * @var string
  833. */
  834. protected $_class_name = 'BModel';
  835. /**
  836. * Read DB connection for selects (replication slave)
  837. *
  838. * @var string|null
  839. */
  840. protected $_readConnectionName;
  841. /**
  842. * Write DB connection for updates (master)
  843. *
  844. * @var string|null
  845. */
  846. protected $_writeConnectionName;
  847. /**
  848. * Read DB name
  849. *
  850. * @var string
  851. */
  852. protected $_readDbName;
  853. /**
  854. * Write DB name
  855. *
  856. * @var string
  857. */
  858. protected $_writeDbName;
  859. /**
  860. * Old values in the object before ->set()
  861. *
  862. * @var array
  863. */
  864. protected $_old_values = array();
  865. /**
  866. * Shortcut factory for generic instance
  867. *
  868. * @param bool $new
  869. * @return BORM
  870. */
  871. public static function i($new=false)
  872. {
  873. if ($new) {
  874. return new static('');
  875. }
  876. if (!static::$_instance) {
  877. static::$_instance = new static('');
  878. }
  879. return static::$_instance;
  880. }
  881. protected function _quote_identifier($identifier) {
  882. if ($identifier[0]=='(') {
  883. return $identifier;
  884. }
  885. return parent::_quote_identifier($identifier);
  886. }
  887. public static function get_config($key)
  888. {
  889. return !empty(static::$_config[$key]) ? static::$_config[$key] : null;
  890. }
  891. /**
  892. * Public alias for _setup_db
  893. */
  894. public static function setup_db()
  895. {
  896. static::_setup_db();
  897. }
  898. /**
  899. * Set up the database connection used by the class.
  900. * Use BPDO for nested transactions
  901. */
  902. protected static function _setup_db()
  903. {
  904. if (!is_object(static::$_db)) {
  905. $connection_string = static::$_config['connection_string'];
  906. $username = static::$_config['username'];
  907. $password = static::$_config['password'];
  908. $driver_options = static::$_config['driver_options'];
  909. if (empty($driver_options[PDO::MYSQL_ATTR_INIT_COMMAND])) { //ADDED
  910. $driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET NAMES utf8";
  911. }
  912. try { //ADDED: hide connection details from the error if not in DEBUG mode
  913. $db = new BPDO($connection_string, $username, $password, $driver_options); //UPDATED
  914. } catch (PDOException $e) {
  915. if (BDebug::is('DEBUG')) {
  916. throw $e;
  917. } else {
  918. throw new PDOException('Could not connect to database');
  919. }
  920. }
  921. $db->setAttribute(PDO::ATTR_ERRMODE, static::$_config['error_mode']);
  922. static::set_db($db);
  923. }
  924. }
  925. /**
  926. * Set the PDO object used by Idiorm to communicate with the database.
  927. * This is public in case the ORM should use a ready-instantiated
  928. * PDO object as its database connection.
  929. */
  930. public static function set_db($db, $config=null)
  931. {
  932. if (!is_null($config)) {
  933. static::$_config = array_merge(static::$_config, $config);
  934. }
  935. static::$_db = $db;
  936. if (!is_null($db)) {
  937. static::_setup_identifier_quote_character();
  938. }
  939. }
  940. /**
  941. * Set read/write DB connection names from model
  942. *
  943. * @param string $read
  944. * @param string $write
  945. * @return BORMWrapper
  946. */
  947. public function set_rw_db_names($read, $write)
  948. {
  949. $this->_readDbName = $read;
  950. $this->_writeDbName = $write;
  951. return $this;
  952. }
  953. protected static function _log_query($query, $parameters)
  954. {
  955. $result = parent::_log_query($query, $parameters);
  956. static::$_last_profile = BDebug::debug('DB.RUN: '.(static::$_last_query ? static::$_last_query : 'LOGGING NOT ENABLED'));
  957. return $result;
  958. }
  959. /**
  960. * Execute the SELECT query that has been built up by chaining methods
  961. * on this class. Return an array of rows as associative arrays.
  962. *
  963. * Connection will be switched to read, if set
  964. *
  965. * @return array
  966. */
  967. protected function _run()
  968. {
  969. BDb::connect($this->_readConnectionName);
  970. #$timer = microtime(true); // file log
  971. $result = parent::_run();
  972. #BDebug::log((microtime(true)-$timer).' '.static::$_last_query); // file log
  973. BDebug::profile(static::$_last_profile);
  974. static::$_last_profile = null;
  975. return $result;
  976. }
  977. /**
  978. * Set or return table alias for the main table
  979. *
  980. * @param string|null $alias
  981. * @return BORM|string
  982. */
  983. public function table_alias($alias=null)
  984. {
  985. if (is_null($alias)) {
  986. return $this->_table_alias;
  987. }
  988. $this->_table_alias = $alias;
  989. return $this;
  990. }
  991. /**
  992. * Add a column to the list of columns returned by the SELECT
  993. * query. This defaults to '*'. The second optional argument is
  994. * the alias to return the column as.
  995. *
  996. * @param string|array $column if array, select multiple columns
  997. * @param string $alias optional alias, if $column is array, used as table name
  998. * @return BORM
  999. */
  1000. public function select($column, $alias=null)
  1001. {
  1002. if (is_array($column)) {
  1003. foreach ($column as $k=>$v) {
  1004. $col = (!is_null($alias) ? $alias.'.' : '').$v;
  1005. if (is_int($k)) {
  1006. $this->select($col);
  1007. } else {
  1008. $this->select($col, $k);
  1009. }
  1010. }
  1011. return $this;
  1012. }
  1013. return parent::select($column, $alias);
  1014. }
  1015. protected $_use_index = array();
  1016. public function use_index($index, $type='USE', $table='_')
  1017. {
  1018. $this->_use_index[$table] = compact('index', 'type');
  1019. return $this;
  1020. }
  1021. protected function _build_select_start() {
  1022. $fragment = parent::_build_select_start();
  1023. if (!empty($this->_use_index['_'])) {
  1024. $idx = $this->_use_index['_'];
  1025. $fragment .= ' '.$idx['type'].' INDEX ('.$idx['index'].') ';
  1026. }
  1027. return $fragment;
  1028. }
  1029. protected function _add_result_column($expr, $alias=null) {
  1030. if (!is_null($alias)) {
  1031. $expr .= " AS " . $this->_quote_identifier($alias);
  1032. }
  1033. // ADDED TO AVOID DUPLICATE FIELDS
  1034. if (in_array($expr, $this->_result_columns)) {
  1035. return $this;
  1036. }
  1037. if ($this->_using_default_result_columns) {
  1038. $this->_result_columns = array($expr);
  1039. $this->_using_default_result_columns = false;
  1040. } else {
  1041. $this->_result_columns[] = $expr;
  1042. }
  1043. return $this;
  1044. }
  1045. public function clear_columns()
  1046. {
  1047. $this->_result_columns = array();
  1048. return $this;
  1049. }
  1050. /**
  1051. * Return select sql statement built from the ORM object
  1052. *
  1053. * @return string
  1054. */
  1055. public function as_sql()
  1056. {
  1057. return $this->_build_select();
  1058. }
  1059. /**
  1060. * Execute the query and return PDO statement object
  1061. *
  1062. * Usage:
  1063. * $sth = $orm->execute();
  1064. * while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { ... }
  1065. *
  1066. * @return PDOStatement
  1067. */
  1068. public function execute()
  1069. {
  1070. BDb::connect($this->_readConnectionName);
  1071. $query = $this->_build_select();
  1072. static::_log_query($query, $this->_values);
  1073. $statement = static::$_db->prepare($query);
  1074. try {
  1075. $statement->execute($this->_values);
  1076. } catch (Exception $e) {
  1077. echo $query;
  1078. print_r($e);
  1079. exit;
  1080. }
  1081. return $statement;
  1082. }
  1083. public function row_to_model($row)
  1084. {
  1085. return $this->_create_model_instance($this->_create_instance_from_row($row));
  1086. }
  1087. /**
  1088. * Iterate over select result with callback on each row
  1089. *
  1090. * @param mixed $callback
  1091. * @param string $type
  1092. * @return BORM
  1093. */
  1094. public function iterate($callback, $type='callback')
  1095. {
  1096. $statement = $this->execute();
  1097. while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
  1098. $model = $this->row_to_model($row);
  1099. switch ($type) {
  1100. case 'callback': call_user_func($callback, $model); break;
  1101. case 'method': $model->$callback(); break;
  1102. }
  1103. }
  1104. return $this;
  1105. }
  1106. /**
  1107. * Extended where condition
  1108. *
  1109. * @param string|array $column_name if array - use where_complex() syntax
  1110. * @param mixed $value
  1111. */
  1112. public function where($column_name, $value=null)
  1113. {
  1114. if (is_array($column_name)) {
  1115. return $this->where_complex($column_name, !!$value);
  1116. }
  1117. return parent::where($column_name, $value);
  1118. }
  1119. /**
  1120. * Add a complex where condition
  1121. *
  1122. * @see BDb::where
  1123. * @param array $conds
  1124. * @param boolean $or
  1125. * @return BORM
  1126. */
  1127. public function where_complex($conds, $or=false)
  1128. {
  1129. list($where, $params) = BDb::where($conds, $or);
  1130. if (!$where) {
  1131. return $this;
  1132. }
  1133. return $this->where_raw($where, $params);
  1134. }
  1135. /**
  1136. * Find one row
  1137. *
  1138. * @param int|null $id
  1139. * @return BModel
  1140. */
  1141. public function find_one($id=null)
  1142. {
  1143. $class = $this->_class_name;
  1144. if ($class::origClass()) {
  1145. $class = $class::origClass();
  1146. }
  1147. BEvents::i()->fire($class.'::find_one:orm', array('orm'=>$this, 'class'=>$class, 'id'=>$id));
  1148. $result = parent::find_one($id);
  1149. BEvents::i()->fire($class.'::find_one:after', array('result'=>&$result, 'class'=>$class, 'id'=>$id));
  1150. return $result;
  1151. }
  1152. /**
  1153. * Find many rows (SELECT)
  1154. *
  1155. * @return array
  1156. */
  1157. public function find_many()
  1158. {
  1159. $class = $this->_class_name;
  1160. if ($class::origClass()) {
  1161. $class = $class::origClass();
  1162. }
  1163. BEvents::i()->fire($class.'::find_many:orm', array('orm'=>$this, 'class'=>$class));
  1164. $result = parent::find_many();
  1165. BEvents::i()->fire($class.'::find_many:after', array('result'=>&$result, 'class'=>$class));
  1166. return $result;
  1167. }
  1168. /**
  1169. * Find many records and return as associated array
  1170. *
  1171. * @param string|array $key if array, will create multi-dimensional array (currently 2D)
  1172. * @param string|null $labelColumn
  1173. * @param array $options (key_lower, key_trim)
  1174. * @return array
  1175. */
  1176. public function find_many_assoc($key=null, $labelColumn=null, $options=array())
  1177. {
  1178. $objects = $this->find_many();
  1179. $array = array();
  1180. if (empty($key)) {
  1181. $key = $this->_get_id_column_name();
  1182. }
  1183. foreach ($objects as $r) {
  1184. $value = is_null($labelColumn) ? $r : (is_array($labelColumn) ? BUtil::arrayMask($r, $labelColumn) : $r->get($labelColumn));
  1185. if (!is_array($key)) { // save on performance for 1D keys
  1186. $v = $r->get($key);
  1187. if (!empty($options['key_lower'])) $v = strtolower($v);
  1188. if (!empty($options['key_trim'])) $v = trim($v);
  1189. $array[$v] = $value;
  1190. } else {
  1191. $v1 = $r->get($key[0]);
  1192. if (!empty($options['key_lower'])) $v1 = strtolower($v1);
  1193. if (!empty($options['key_trim'])) $v1 = trim($v1);
  1194. $v2 = $r->get($key[1]);
  1195. if (!empty($options['key_lower'])) $v2 = strtolower($v2);
  1196. if (!empty($options['key_trim'])) $v1 = trim($v2);
  1197. $array[$v1][$v2] = $value;
  1198. }
  1199. }
  1200. return $array;
  1201. }
  1202. /**
  1203. * Check whether the given field (or object itself) has been changed since this
  1204. * object was saved.
  1205. */
  1206. public function is_dirty($key=null) {
  1207. return is_null($key) ? !empty($this->_dirty_fields) : isset($this->_dirty_fields[$key]);
  1208. }
  1209. /**
  1210. * Set a property to a particular value on this object.
  1211. * Flags that property as 'dirty' so it will be saved to the
  1212. * database when save() is called.
  1213. */
  1214. public function set($key, $value) {
  1215. if (!is_scalar($key)) {
  1216. throw new BException('Key not scalar');
  1217. }
  1218. if (!array_key_exists($key, $this->_data)
  1219. || is_null($this->_data[$key]) && !is_null($value)
  1220. || !is_null($this->_data[$key]) && is_null($value)
  1221. || is_scalar($this->_data[$key]) && is_scalar($value)
  1222. && ((string)$this->_data[$key] !== (string)$value)
  1223. ) {
  1224. #echo "DIRTY: "; var_dump($this->_data[$key], $value); echo "\n";
  1225. if (!array_key_exists($key, $this->_old_values)) {
  1226. $this->_old_values[$key] = array_key_exists($key, $this->_data) ? $this->_data[$key] : BNULL;
  1227. }
  1228. $this->_dirty_fields[$key] = $value;
  1229. }
  1230. $this->_data[$key] = $value;
  1231. }
  1232. /**
  1233. * Class to table map cache
  1234. *
  1235. * @var array
  1236. */
  1237. protected static $_classTableMap = array();
  1238. /**
  1239. * Add a simple JOIN source to the query
  1240. */
  1241. public function _add_join_source($join_operator, $table, $constraint, $table_alias=null) {
  1242. if (!isset(self::$_classTableMap[$table])) {
  1243. if (class_exists($table) && is_subclass_of($table, 'BModel')) {
  1244. $class = BClassRegistry::i()->className($table);
  1245. self::$_classTableMap[$table] = $class::table();
  1246. } else {
  1247. self::$_classTableMap[$table] = false;
  1248. }
  1249. }
  1250. if (self::$_classTableMap[$table]) {
  1251. $table = self::$_classTableMap[$table];
  1252. }
  1253. return parent::_add_join_source($join_operator, $table, $constraint, $table_alias);
  1254. }
  1255. /**
  1256. * Save any fields which have been modified on this object
  1257. * to the database.
  1258. *
  1259. * Connection will be switched to write, if set
  1260. *
  1261. * @return boolean
  1262. */
  1263. public function save()
  1264. {
  1265. BDb::connect($this->_writeConnectionName);
  1266. $this->_dirty_fields = BDb::cleanForTable($this->_table_name, $this->_dirty_fields);
  1267. if (true) {
  1268. #if (array_diff_assoc($this->_old_values, $this->_dirty_fields)) {
  1269. $result = parent::save();
  1270. #}
  1271. } else {
  1272. echo $this->_class_name.'['.$this->id.']: ';
  1273. print_r($this->_data);
  1274. echo 'FROM: '; print_r($this->_old_values);
  1275. echo 'TO: '; print_r($this->_dirty_fields); echo "\n\n";
  1276. $result = true;
  1277. }
  1278. //$this->_old_values = array(); // commented out to make original loaded object old values available after save
  1279. return $result;
  1280. }
  1281. /**
  1282. * Return dirty fields for debugging
  1283. *
  1284. * @return array
  1285. */
  1286. public function dirty_fields()
  1287. {
  1288. return $this->_dirty_fields;
  1289. }
  1290. public function old_values($property='')
  1291. {
  1292. if ($property && isset($this->_old_values[$property])) {
  1293. return $this->_old_values[$property];
  1294. }
  1295. return $this->_old_values;
  1296. }
  1297. /**
  1298. * Delete this record from the database
  1299. *
  1300. * Connection will be switched to write, if set
  1301. *
  1302. * @return boolean
  1303. */
  1304. public function delete()
  1305. {
  1306. BDb::connect($this->_writeConnectionName);
  1307. return parent::delete();
  1308. }
  1309. /**
  1310. * Add an ORDER BY expression DESC clause
  1311. */
  1312. public function order_by_expr($expression) {
  1313. $this->_order_by[] = "{$expression}";
  1314. return $this;
  1315. }
  1316. /**
  1317. * Perform a raw query. The query should contain placeholders,
  1318. * in either named or question mark style, and the parameters
  1319. * should be an array of values which will be bound to the
  1320. * placeholders in the query. If this method is called, all
  1321. * other query building methods will be ignored.
  1322. *
  1323. * Connection will be set to write, if query is not SELECT or SHOW
  1324. *
  1325. * @param $query
  1326. * @param array $parameters
  1327. * @return BORM
  1328. */
  1329. public function raw_query($query, $parameters=array())
  1330. {
  1331. if (preg_match('#^\s*(SELECT|SHOW)#i', $query)) {
  1332. BDb::connect($this->_readConnectionName);
  1333. } else {
  1334. BDb::connect($this->_writeConnectionName);
  1335. }
  1336. return parent::raw_query($query, $parameters);
  1337. }
  1338. /**
  1339. * Get table name with prefix, if configured
  1340. *
  1341. * @param string $class_name
  1342. * @return string
  1343. */
  1344. protected static function _get_table_name($class_name) {
  1345. return BDb::t(parent::_get_table_name($class_name));
  1346. }
  1347. /**
  1348. * Set page constraints on collection for use in grids
  1349. *
  1350. * Request and result vars:
  1351. * - p: page number
  1352. * - ps: page size
  1353. * - s: sort order by (if default is array - only these values are allowed) (alt: sort|dir)
  1354. * - sd: sort direction (asc/desc)
  1355. * - sc: sort combined (s|sd)
  1356. * - rs: requested row start (optional in request, not dependent on page size)
  1357. * - rc: requested row count (optional in request, not dependent on page size)
  1358. * - c: total row count (return only)
  1359. * - mp: max page (return only)
  1360. *
  1361. * Options (all optional):
  1362. * - format: 0..2
  1363. * - as_array: true or method name
  1364. *
  1365. * @param array $r pagination request, if null - take from request query string
  1366. * @param array $d default values and options
  1367. * @return array
  1368. */
  1369. public function paginate($r=null, $d=array())
  1370. {
  1371. if (is_null($r)) {
  1372. $r = BRequest::i()->request(); // GET request
  1373. }
  1374. $d = (array)$d; // make sure it's array
  1375. if (!empty($r['sc']) && empty($r['s']) && empty($r['sd'])) { // sort and dir combined
  1376. list($r['s'], $r['sd']) = preg_split('#[| ]#', trim($r['sc']));
  1377. }
  1378. if (!empty($r['s']) && !empty($d['s']) && is_array($d['s'])) { // limit by these values only
  1379. if (!in_array($r['s'], $d['s'])) $r['s'] = null;
  1380. $d['s'] = null;
  1381. }
  1382. if (!empty($r['sd']) && $r['sd']!='asc' && $r['sd']!='desc') { // only asc and desc are allowed
  1383. $r['sd'] = null;
  1384. }
  1385. $s = array( // state
  1386. 'p' => !empty($r['p']) && is_numeric($r['p']) ? $r['p'] : (isset($d['p']) ? $d['p'] : 1), // page
  1387. 'ps' => !empty($r['ps']) && is_numeric($r['ps']) ? $r['ps'] : (isset($d['ps']) ? $d['ps'] : 100), // page size
  1388. 's' => !empty($r['s']) ? $r['s'] : (isset($d['s']) ? $d['s'] : ''), // sort by
  1389. 'sd' => !empty($r['sd']) ? $r['sd'] : (isset($d['sd']) ? $d['sd'] : 'asc'), // sort dir
  1390. 'rs' => !empty($r['rs']) ? $r['rs'] : null, // starting row
  1391. 'rc' => !empty($r['rc']) ? $r['rc'] : null, // total rows on page
  1392. 'q' => !empty($r['q']) ? $r['q'] : null, // query string
  1393. 'c' => !empty($d['c']) ? $d['c'] : null, //total found
  1394. );
  1395. #print_r($r); print_r($d); print_r($s); exit;
  1396. $s['sc'] = $s['s'].' '.$s['sd']; // sort combined for state
  1397. #$s['c'] = 600000;
  1398. if (empty($s['c'])){
  1399. $cntOrm = clone $this; // clone ORM to count
  1400. $s['c'] = $cntOrm->count(); // total row count
  1401. unset($cntOrm); // free mem
  1402. }
  1403. $s['mp'] = ceil($s['c']/$s['ps']); // max page
  1404. if (($s['p']-1)*$s['ps']>$s['c']) $s['p'] = $s['mp']; // limit to max page
  1405. if ($s['s']) $this->{'order_by_'.$s['sd']}($s['s']); // sort rows if requested
  1406. $s['rs'] = max(0, isset($s['rs']) ? $s['rs'] : ($s['p']-1)*$s['ps']); // start from requested row or page
  1407. if(empty($d['donotlimit'])){
  1408. $this->offset($s['rs'])->limit(!empty($s['rc']) ? $s['rc'] : $s['ps']); // limit rows to page
  1409. }
  1410. #BDebug::dump($s);
  1411. #BDebug::dump($this);
  1412. $rows = $this->find_many(); // result data
  1413. $s['rc'] = $rows ? sizeof($rows) : 0; // returned row count
  1414. if (!empty($d['as_array'])) {
  1415. $rows = BDb::many_as_array($rows, is_string($d['as_array']) ? $d['as_array'] : 'as_array');
  1416. }
  1417. if (!empty($d['format'])) {
  1418. switch ($d['format']) {
  1419. case 1: return $rows;
  1420. case 2: $s['rows'] = $rows; return $s;
  1421. }
  1422. }
  1423. return array('state'=>$s, 'rows'=>$rows);
  1424. }
  1425. public function jqGridData($r=null, $d=array())
  1426. {
  1427. if (is_null($r)) {
  1428. $r = BRequest::i()->request();
  1429. }
  1430. if (!empty($r['rows'])) { // without adapting jqgrid config
  1431. $data = $this->paginate(array(
  1432. 'p' => !empty($r['page']) ? $r['page'] : null,
  1433. 'ps' => !empty($r['rows']) ? $r['rows'] : null,
  1434. 's' => !empty($r['sidx']) ? $r['sidx'] : null,
  1435. 'sd' => !empty($r['sord']) ? $r['sord'] : null,
  1436. ), $d);
  1437. } else { // jqgrid config adapted
  1438. $data = $this->paginate($r, $d);
  1439. }
  1440. $res = $data['state'];
  1441. $res['rows'] = $data['rows'];
  1442. if (empty($d['as_array'])) {
  1443. $res['rows'] = BDb::many_as_array($res['rows']);
  1444. }
  1445. return $res;
  1446. }
  1447. public function __destruct()
  1448. {
  1449. unset($this->_data);
  1450. }
  1451. }
  1452. /**
  1453. * ORM model base class
  1454. */
  1455. class BModel extends Model
  1456. {
  1457. /**
  1458. * Original class to be used as event prefix to remain constant in overridden classes
  1459. *
  1460. * Usage:
  1461. *
  1462. * class Some_Class extends BClass
  1463. * {
  1464. * static protected $_origClass = __CLASS__;
  1465. * }
  1466. *
  1467. * @var string
  1468. */
  1469. static protected $_origClass;
  1470. /**
  1471. * Named connection reference
  1472. *
  1473. * @var string
  1474. */
  1475. protected static $_connectionName = 'DEFAULT';
  1476. /**
  1477. * DB name for reads. Set in class declaration
  1478. *
  1479. * @var string|null
  1480. */
  1481. protected static $_readConnectionName = null;
  1482. /**
  1483. * DB name for writes. Set in class declaration
  1484. *
  1485. * @var string|null
  1486. */
  1487. protected static $_writeConnectionName = null;
  1488. /**
  1489. * Final table name cache with prefix
  1490. *
  1491. * @var array
  1492. */
  1493. protected static $_tableNames = array();
  1494. /**
  1495. * Whether to enable automatic model caching on load
  1496. * if array, auto cache only if loading by one of these fields
  1497. *
  1498. * @var boolean|array
  1499. */
  1500. protected static $_cacheAuto = false;
  1501. /**
  1502. * Fields used in cache, that require values to be case insensitive or trimmed
  1503. *
  1504. * - key_lower
  1505. * - key_trim (TODO)
  1506. *
  1507. * @var array
  1508. */
  1509. protected static $_cacheFlags = array();
  1510. /**
  1511. * Cache of model instances (for models that makes sense to keep cache)
  1512. *
  1513. * @var array
  1514. */
  1515. protected static $_cache = array();
  1516. /**
  1517. * Cache of instance level data values (related models)
  1518. *
  1519. * @var array
  1520. */
  1521. protected static $_instanceCache = array();
  1522. /**
  1523. * TRUE after save if a new record
  1524. *
  1525. * @var boolean
  1526. */
  1527. protected $_newRecord;
  1528. /**
  1529. * Rules for model data validation using BValidate
  1530. *
  1531. * @var array
  1532. */
  1533. protected $_validationRules = array();
  1534. /**
  1535. * Retrieve original class name
  1536. *
  1537. * @return string
  1538. */
  1539. public static function origClass()
  1540. {
  1541. return static::$_origClass;
  1542. }
  1543. /**
  1544. * PDO object of read DB connection
  1545. *
  1546. * @return BPDO
  1547. */
  1548. public static function readDb()
  1549. {
  1550. return BDb::connect(static::$_readConnectionName ? static::$_readConnectionName : static::$_connectionName);
  1551. }
  1552. /**
  1553. * PDO object of write DB connection
  1554. *
  1555. * @return BPDO
  1556. */
  1557. public static function writeDb()
  1558. {
  1559. return BDb::connect(static::$_writeConnectionName ? static::$_writeConnectionName : static::$_connectionName);
  1560. }
  1561. /**
  1562. * Model instance factory
  1563. *
  1564. * Use XXX::i()->orm($alias) instead
  1565. *
  1566. * @param string|null $class_name optional
  1567. * @return BORM
  1568. */
  1569. public static function factory($class_name=null)
  1570. {
  1571. if (is_null($class_name)) { // ADDED
  1572. $class_name = get_called_class();
  1573. }
  1574. $class_name = BClassRegistry::i()->className($class_name); // ADDED
  1575. static::readDb();
  1576. $table_name = static::_get_table_name($class_name);
  1577. $orm = BORM::for_table($table_name); // CHANGED
  1578. $orm->set_class_name($class_name);
  1579. $orm->use_id_column(static::_get_id_column_name($class_name));
  1580. $orm->set_rw_db_names( // ADDED
  1581. static::$_readConnectionName ? static::$_readConnectionName : static::$_connectionName,
  1582. static::$_writeConnectionName ? static::$_writeConnectionName : static::$_connectionName
  1583. );
  1584. $orm->table_alias('_main');
  1585. return $orm;
  1586. }
  1587. /**
  1588. * Alias for self::factory() with shortcut for table alias
  1589. *
  1590. * @param string $alias table alias
  1591. * @return BORM
  1592. */
  1593. public static function orm($alias=null)
  1594. {
  1595. $orm = static::factory();
  1596. if ($alias) {
  1597. $orm->table_alias($alias);
  1598. }
  1599. static::_findOrm($orm);
  1600. return $orm;
  1601. }
  1602. /**
  1603. * Placeholder for class specific ORM augmentation
  1604. *
  1605. * @param BORM $orm
  1606. */
  1607. protected static function _findOrm($orm)
  1608. {
  1609. }
  1610. /**
  1611. * Fallback singleton/instance factory
  1612. *
  1613. * @param bool $new if true returns a new instance, otherwise singleton
  1614. * @param array $args
  1615. * @return BClass
  1616. */
  1617. public static function i($new=false, array $args=array())
  1618. {
  1619. return BClassRegistry::i()->instance(get_called_class(), $args, !$new);
  1620. }
  1621. /**
  1622. * Enhanced set method, allowing to set multiple values, and returning $this for chaining
  1623. *
  1624. * @param string|array $key
  1625. * @param mixed $value
  1626. * @param mixed $flag if true, add to existing value; if null, update only if currently not set
  1627. * @return BModel
  1628. */
  1629. public function set($key, $value=null, $flag=false)
  1630. {
  1631. if (is_array($key)) {
  1632. foreach ($key as $k=>$v) {
  1633. parent::set($k, $v);
  1634. }
  1635. } else {
  1636. if (true===$flag) {
  1637. $oldValue = $this->get($key);
  1638. if (is_array($oldValue)) {
  1639. $oldValue[] = $value;
  1640. $value = $oldValue;
  1641. } else {
  1642. $value += $oldValue;
  1643. }
  1644. }
  1645. if (is_scalar($key) && (!is_null($flag) || is_null($this->get($key)))) {
  1646. parent::set($key, $value);
  1647. }
  1648. }
  1649. return $this;
  1650. }
  1651. /**
  1652. * Add a value to field
  1653. *
  1654. * @param string $key
  1655. * @param mixed $value
  1656. * @return BModel
  1657. */
  1658. public function add($key, $increment=1)
  1659. {
  1660. return $this->set($key, $increment, true);
  1661. }
  1662. /**
  1663. * Create a new instance of the model
  1664. *
  1665. * @param null|array $data
  1666. */
  1667. public static function create($data=null)
  1668. {
  1669. $record = static::factory()->create($data);
  1670. $record->onAfterCreate();
  1671. return $record;
  1672. }
  1673. /**
  1674. * Placeholder for after creae callback
  1675. *
  1676. * Called not after new object save, but after creation of the object in memory
  1677. */
  1678. public function onAfterCreate()
  1679. {
  1680. BEvents::i()->fire($this->_origClass().'::onAfterCreate', array('model' => $this));
  1681. return $this;
  1682. }
  1683. /**
  1684. * Get event class prefix for current object
  1685. *
  1686. * @return string
  1687. */
  1688. protected function _origClass()
  1689. {
  1690. return static::$_origClass ? static::$_origClass : get_class($this);
  1691. }
  1692. /**
  1693. * Place holder for custom load ORM logic
  1694. *
  1695. * @param BORM $orm
  1696. */
  1697. protected static function _loadORM($orm)
  1698. {
  1699. }
  1700. /**
  1701. * Load a model object based on ID, another field or multiple fields
  1702. *
  1703. * @param int|string|array $id
  1704. * @param string $field
  1705. * @param boolean $cache
  1706. * @return BModel
  1707. */
  1708. public static function load($id, $field=null, $cache=false)
  1709. {
  1710. $class = static::$_origClass ? static::$_origClass : get_called_class();
  1711. if (is_null($field)) {
  1712. $field = static::_get_id_column_name($class);
  1713. }
  1714. if (is_array($id)) {
  1715. ksort($id);
  1716. $field = join(',', array_keys($id));
  1717. $keyValue = join(',', array_values($id));
  1718. } else {
  1719. $keyValue = $id;
  1720. }
  1721. if (!empty(static::$_cacheFlags[$field]['key_lower'])) {
  1722. $keyValue = strtolower($keyValue);
  1723. }
  1724. if (!empty(static::$_cache[$class][$field][$keyValue])) {
  1725. return static::$_cache[$class][$field][$keyValue];
  1726. }
  1727. $orm = static::factory();
  1728. static::_loadORM($orm);
  1729. BEvents::i()->fire($class.'::load:orm', array('orm'=>$orm, 'class'=>$class, 'called_class'=>get_called_class()));
  1730. if (is_array($id)) {
  1731. $orm->where_complex($id);
  1732. } else {
  1733. if (strpos($field, '.')===false && ($alias = $orm->table_alias())) {
  1734. $field = $alias.'.'.$field;
  1735. }
  1736. $orm->where($field, $id);
  1737. }
  1738. /** @var BModel $record */
  1739. $model = $orm->find_one();
  1740. if ($model) {
  1741. $model->onAfterLoad();
  1742. if ($cache
  1743. || static::$_cacheAuto===true
  1744. || is_array(static::$_cacheAuto) && in_array($field, static::$_cacheAuto)
  1745. ) {
  1746. $model->cacheStore();
  1747. }
  1748. }
  1749. return $model;
  1750. }
  1751. /**
  1752. * Placeholder for after load callback
  1753. *
  1754. * @return BModel
  1755. */
  1756. public function onAfterLoad()
  1757. {
  1758. BEvents::i()->fire($this->_origClass().'::onAfterLoad', array('model'=>$this));
  1759. return $this;
  1760. }
  1761. /**
  1762. * Apply afterLoad() to all models in collection
  1763. *
  1764. * @param array $arr Model collection
  1765. * @return BModel
  1766. */
  1767. public function mapAfterLoad($arr)
  1768. {
  1769. foreach ($arr as $r) {
  1770. $r->onAfterLoad();
  1771. }
  1772. return $this;
  1773. }
  1774. /**
  1775. * Clear model cache
  1776. *
  1777. * @return BModel
  1778. */
  1779. public function cacheClear()
  1780. {
  1781. static::$_cache[$this->_origClass()] = array();
  1782. return $this;
  1783. }
  1784. /**
  1785. * Preload models into cache
  1786. *
  1787. * @param mixed $where complex where @see BORM::where_complex()
  1788. * @param mixed $field cache key
  1789. * @param mixed $sort
  1790. * @return BModel
  1791. */
  1792. public function cachePreload($where=null, $field=null, $sort=null)
  1793. {
  1794. $orm = static::factory();
  1795. $class = $this->_origClass();
  1796. if (is_null($field)) {
  1797. $field = static::_get_id_column_name($class);
  1798. }
  1799. $cache =& static::$_cache[$class];
  1800. if ($where) $orm->where_complex($where);
  1801. if ($sort) $orm->order_by_asc($sort);
  1802. $options = !empty(static::$_cacheFlags[$field]) ? static::$_cacheFlags[$field] : array();
  1803. $cache[$field] = $orm->find_many_assoc($field, null, $options);
  1804. return $this;
  1805. }
  1806. /**
  1807. * Preload models using keys from external collection
  1808. *
  1809. * @param array $collection
  1810. * @param string $fk foreign key field
  1811. * @param string $lk local key field
  1812. * @return BModel
  1813. */
  1814. public function cachePreloadFrom($collection, $fk='id', $lk='id')
  1815. {
  1816. if (!$collection) return $this;
  1817. $class = $this->_origClass();
  1818. $keyValues = array();
  1819. $keyLower = !empty(static::$_cacheFlags[$lk]['key_lower']);
  1820. foreach ($collection as $r) {
  1821. $key = null;
  1822. if (is_object($r)) {
  1823. $keyValue = $r->get($fk);
  1824. } elseif (is_array($r)) {
  1825. $keyValue = isset($r[$fk]) ? $r[$fk] : null;
  1826. } elseif (is_scalar($r)) {
  1827. $keyValue = $r;
  1828. }
  1829. if (empty($keyValue)) continue;
  1830. if ($keyLower) $keyValue = strtolower($keyValue);
  1831. if (!empty(static::$_cache[$class][$lk][$keyValue])) continue;
  1832. $keyValues[$keyValue] = 1;
  1833. }
  1834. $field = (strpos($lk, '.')===false ? '_main.' : '').$lk; //TODO: table alias flexibility
  1835. if ($keyValues) $this->cachePreload(array($field=>array_keys($keyValues)), $lk);
  1836. return $this;
  1837. }
  1838. /**
  1839. * Copy cache into another field
  1840. *
  1841. * @param string $toKey
  1842. * @param string $fromKey
  1843. * @return BModel
  1844. */
  1845. public function cacheCopy($toKey, $fromKey='id')
  1846. {
  1847. $cache =& static::$_cache[$this->_origClass()];
  1848. $lower = !empty(static::$_cacheFlags[$toKey]['key_lower']);
  1849. foreach ($cache[$fromKey] as $r) {
  1850. $keyValue = $r->get($toKey);
  1851. if ($lower) $keyValue = strtolower($keyValue);
  1852. $cache[$toKey][$keyValue] = $r;
  1853. }
  1854. return $this;
  1855. }
  1856. /**
  1857. * Save all dirty models in cache
  1858. *
  1859. * @return BModel
  1860. */
  1861. public function cacheSaveDirty($field='id')
  1862. {
  1863. $class = $this->_origClass();
  1864. if (!empty(static::$_cache[$class][$field])) {
  1865. foreach (static::$_cache[$class][$field] as $c) {
  1866. if ($c->is_dirty()) {
  1867. $c->save();
  1868. }
  1869. }
  1870. }
  1871. return $this;
  1872. }
  1873. /**
  1874. * Fetch all cached models by field
  1875. *
  1876. * @param string $field
  1877. * @param string $key
  1878. * @return array|BModel
  1879. */
  1880. public function cacheFetch($field='id', $keyValue=null)
  1881. {
  1882. $class = $this->_origClass();
  1883. if (empty(static::$_cache[$class])) return null;
  1884. $cache = static::$_cache[$class];
  1885. if (empty($cache[$field])) return null;
  1886. if (is_null($keyValue)) return $cache[$field];
  1887. if (!empty(static::$_cacheFlags[$field]['key_lower'])) $keyValue = strtolower($keyValue);
  1888. return !empty($cache[$field][$keyValue]) ? $cache[$field][$keyValue] : null;
  1889. }
  1890. /**
  1891. * Store model in cache by field
  1892. *
  1893. * @todo rename to cacheSave()
  1894. *
  1895. * @param string|array $field one or more fields to store the cache for
  1896. * @param array $collection external model collection to store into cache
  1897. * @return BModel
  1898. */
  1899. public function cacheStore($field='id', $collection=null)
  1900. {
  1901. $cache =& static::$_cache[$this->_origClass()];
  1902. if ($collection) {
  1903. foreach ($collection as $r) {
  1904. $r->cacheStore($field);
  1905. }
  1906. return $this;
  1907. }
  1908. if (is_array($field)) {
  1909. foreach ($field as $k) {
  1910. $this->cacheStore($k);
  1911. }
  1912. return $this;
  1913. }
  1914. if (strpos($field, ',')) {
  1915. $keyValueArr = array();
  1916. foreach (explode(',', $field) as $k) {
  1917. $keyValueArr[] = $this->get($k);
  1918. }
  1919. $keyValue = join(',', $keyValueArr);
  1920. } else {
  1921. $keyValue = $this->get($field);
  1922. }
  1923. if (!empty(static::$_cacheFlags[$field]['key_lower'])) $keyValue = strtolower($keyValue);
  1924. $cache[$field][$keyValue] = $this;
  1925. return $this;
  1926. }
  1927. /**
  1928. * Placeholder for before save callback
  1929. *
  1930. * @return boolean whether to continue with save
  1931. */
  1932. public function onBeforeSave()
  1933. {
  1934. BEvents::i()->fire($this->origClass().'::onBeforeSave', array('model'=>$this));
  1935. return true;
  1936. }
  1937. /**
  1938. * Return dirty fields for debugging
  1939. *
  1940. * @return array
  1941. */
  1942. public function dirty_fields()
  1943. {
  1944. return $this->orm->dirty_fields();
  1945. }
  1946. /**
  1947. * Check whether the given field has changed since the object was created or saved
  1948. */
  1949. public function is_dirty($property=null) {
  1950. return $this->orm->is_dirty($property);
  1951. }
  1952. /**
  1953. * Return old value(s) of modified field
  1954. * @param type $property
  1955. * @return type
  1956. */
  1957. public function old_values($property='')
  1958. {
  1959. return $this->orm->old_values($property);
  1960. }
  1961. /**
  1962. * Save method returns the model object for chaining
  1963. *
  1964. *
  1965. * @param boolean $callBeforeAfter whether to call onBeforeSave and onAfterSave methods
  1966. * @return BModel
  1967. */
  1968. public function save($callBeforeAfter=true)
  1969. {
  1970. if ($callBeforeAfter) {
  1971. try {
  1972. if (!$this->onBeforeSave()) {
  1973. return $this;
  1974. }
  1975. } catch (BModelException $e) {
  1976. return $this;
  1977. }
  1978. }
  1979. $this->_newRecord = !$this->get(static::_get_id_column_name(get_called_class()));
  1980. parent::save();
  1981. if ($callBeforeAfter) {
  1982. $this->onAfterSave();
  1983. }
  1984. if (static::$_cacheAuto) {
  1985. $this->cacheStore();
  1986. }
  1987. return $this;
  1988. }
  1989. /**
  1990. * Placeholder for after save callback
  1991. *
  1992. */
  1993. public function onAfterSave()
  1994. {
  1995. BEvents::i()->fire($this->_origClass().'::onAfterSave', array('model'=>$this));
  1996. return $this;
  1997. }
  1998. /**
  1999. * Was the record just saved to DB?
  2000. *
  2001. * @return boolean
  2002. */
  2003. public function isNewRecord()
  2004. {
  2005. return $this->_newRecord;
  2006. }
  2007. /**
  2008. * Placeholder for before delete callback
  2009. *
  2010. * @return boolean whether to continue with delete
  2011. */
  2012. public function onBeforeDelete()
  2013. {
  2014. BEvents::i()->fire($this->_origClass().'::onBeforeDelete', array('model'=>$this));
  2015. return true;
  2016. }
  2017. public function delete()
  2018. {
  2019. try {
  2020. if (!$this->onBeforeDelete()) {
  2021. return $this;
  2022. }
  2023. } catch(BModelException $e) {
  2024. return $this;
  2025. }
  2026. if (($cache =& static::$_cache[$this->_origClass()])) {
  2027. foreach ($cache as $k=>$c) {
  2028. $keyValue = $this->get($k);
  2029. if (!empty(static::$_cacheFlags[$k]['key_lower'])) $keyValue = strtolower($keyValue);
  2030. unset($cache[$k][$keyValue]);
  2031. }
  2032. }
  2033. parent::delete();
  2034. $this->onAfterDelete();
  2035. return $this;
  2036. }
  2037. public function onAfterDelete()
  2038. {
  2039. BEvents::i()->fire($this->_origClass().'::onAfterDelete', array('model'=>$this));
  2040. return $this;
  2041. }
  2042. /**
  2043. * Run raw SQL with optional parameters
  2044. *
  2045. * @param string $sql
  2046. * @param array $params
  2047. * @return PDOStatement
  2048. */
  2049. public static function run_sql($sql, $params=array())
  2050. {
  2051. return static::writeDb()->prepare($sql)->execute((array)$params);
  2052. }
  2053. /**
  2054. * Get table name for the model
  2055. *
  2056. * @return string
  2057. */
  2058. public static function table()
  2059. {
  2060. $class = BClassRegistry::i()->className(get_called_class());
  2061. if (empty(static::$_tableNames[$class])) {
  2062. static::$_tableNames[$class] = static::_get_table_name($class);
  2063. }
  2064. return static::$_tableNames[$class];
  2065. }
  2066. public static function overrideTable($table)
  2067. {
  2068. static::$_table = $table;
  2069. $class = get_called_class();
  2070. BDebug::debug('OVERRIDE TABLE: '.$class.' -> '.$table);
  2071. static::$_tableNames[$class] = null;
  2072. $class = BClassRegistry::i()->className($class);
  2073. static::$_tableNames[$class] = null;
  2074. }
  2075. /**
  2076. * Update one or many records of the class
  2077. *
  2078. * @param array $data
  2079. * @param string|array $where where conditions (@see BDb::where)
  2080. * @param array $params if $where string, use these params
  2081. * @return boolean
  2082. */
  2083. public static function update_many(array $data, $where, $p=array())
  2084. {
  2085. $update = array();
  2086. $params = array();
  2087. foreach ($data as $k=>$v) {
  2088. $update[] = "`{$k}`=?";
  2089. $params[] = $v;
  2090. }
  2091. if (is_array($where)) {
  2092. list($where, $p) = BDb::where($where);
  2093. }
  2094. $sql = "UPDATE ".static::table()." SET ".join(', ', $update) ." WHERE {$where}";
  2095. BDebug::debug('SQL: '.$sql);
  2096. return static::run_sql($sql, array_merge($params, $p));
  2097. }
  2098. /**
  2099. * Delete one or many records of the class
  2100. *
  2101. * @param string|array $where where conditions (@see BDb::where)
  2102. * @param array $params if $where string, use these params
  2103. * @return boolean
  2104. */
  2105. public static function delete_many($where, $params=array())
  2106. {
  2107. if (is_array($where)) {
  2108. list($where, $params) = BDb::where($where);
  2109. }
  2110. $sql = "DELETE FROM ".static::table()." WHERE {$where}";
  2111. BDebug::debug('SQL: '.$sql);
  2112. return static::run_sql($sql, $params);
  2113. }
  2114. /**
  2115. * Model data as array, recursively
  2116. *
  2117. * @param array $objHashes cache of object hashes to check for infinite recursion
  2118. * @return array
  2119. */
  2120. public function as_array(array $objHashes=array())
  2121. {
  2122. $objHash = spl_object_hash($this);
  2123. if (!empty($objHashes[$objHash])) {
  2124. return "*** RECURSION: ".get_class($this);
  2125. }
  2126. $objHashes[$objHash] = 1;
  2127. $data = parent::as_array();
  2128. foreach ($data as $k=>$v) {
  2129. if ($v instanceof Model) {
  2130. $data[$k] = $v->as_array();
  2131. } elseif (is_array($v) && current($v) instanceof Model) {
  2132. foreach ($v as $k1=>$v1) {
  2133. $data[$k][$k1] = $v1->as_array($objHashes);
  2134. }
  2135. }
  2136. }
  2137. return $data;
  2138. }
  2139. /**
  2140. * Store instance data cache, such as related models
  2141. *
  2142. * @deprecated
  2143. * @param string $key
  2144. * @param mixed $value
  2145. * @return mixed
  2146. */
  2147. public function instanceCache($key, $value=null)
  2148. {
  2149. $thisHash = spl_object_hash($this);
  2150. if (null===$value) {
  2151. return isset(static::$_instanceCache[$thisHash][$key]) ? static::$_instanceCache[$thisHash][$key] : null;
  2152. }
  2153. static::$_instanceCache[$thisHash][$key] = $value;
  2154. return $this;
  2155. }
  2156. public function saveInstanceCache($key, $value)
  2157. {
  2158. $thisHash = spl_object_hash($this);
  2159. static::$_instanceCache[$thisHash][$key] = $value;
  2160. return $this;
  2161. }
  2162. public function loadInstanceCache($key)
  2163. {
  2164. $thisHash = spl_object_hash($this);
  2165. return isset(static::$_instanceCache[$thisHash][$key]) ? static::$_instanceCache[$thisHash][$key] : null;
  2166. }
  2167. /**
  2168. * Retrieve persistent related model object
  2169. *
  2170. * @param string $modelClass
  2171. * @param mixed $idValue related object id value or complex where expression
  2172. * @param boolean $autoCreate if record doesn't exist yet, create a new object
  2173. * @result BModel
  2174. */
  2175. public function relatedModel($modelClass, $idValue, $autoCreate=false, $cacheKey=null, $foreignIdField='id')
  2176. {
  2177. $cacheKey = $cacheKey ? $cacheKey : $modelClass;
  2178. $model = $this->loadInstanceCache($cacheKey);
  2179. if (is_null($model)) {
  2180. if (is_array($idValue)) {
  2181. $model = $modelClass::i()->factory()->where_complex($idValue)->find_one();
  2182. if ($model) $model->afterLoad();
  2183. } else {
  2184. $model = $modelClass::i()->load($idValue);
  2185. }
  2186. if ($autoCreate && !$model) {
  2187. if (is_array($idValue)) {
  2188. $model = $modelClass::i()->create($idValue);
  2189. } else {
  2190. $model = $modelClass::i()->create(array($foreignIdField=>$idValue));
  2191. }
  2192. }
  2193. $this->saveInstanceCache($cacheKey, $model);
  2194. }
  2195. return $model;
  2196. }
  2197. /**
  2198. * Retrieve persistent related model objects collection
  2199. *
  2200. * @param string $modelClass
  2201. * @param mixed $idValue complex where expression
  2202. * @result array
  2203. */
  2204. public function relatedCollection($modelClass, $where)
  2205. {
  2206. }
  2207. /**
  2208. * Return a member of child collection identified by a field
  2209. *
  2210. * @param string $var
  2211. * @param string|int $id
  2212. * @param string $idField
  2213. * @return mixed
  2214. */
  2215. public function childById($var, $id, $idField='id')
  2216. {
  2217. $collection = $this->get($var);
  2218. if (!$collection){
  2219. $collection = $this->{$var};
  2220. if (!$collection) return null;
  2221. }
  2222. foreach ($collection as $k=>$v) {
  2223. if ($v->get($idField)==$id) return $v;
  2224. }
  2225. return null;
  2226. }
  2227. public function __destruct()
  2228. {
  2229. if ($this->orm) {
  2230. $class = $this->_origClass();
  2231. if (!empty(static::$_cache[$class])) {
  2232. foreach (static::$_cache[$class] as $key=>$cache) {
  2233. $keyValue = $this->get($key);
  2234. if (!empty($cache[$keyValue])) {
  2235. unset(static::$_cache[$class][$keyValue]);
  2236. }
  2237. }
  2238. }
  2239. unset(static::$_instanceCache[spl_object_hash($this)]);
  2240. }
  2241. }
  2242. public function fieldOptions($field=null, $key=null)
  2243. {
  2244. if (is_null($field)) {
  2245. return static::$_fieldOptions;
  2246. }
  2247. if (!isset(static::$_fieldOptions[$field])) {
  2248. BDebug::warning('Invalid field options type: '.$field);
  2249. return null;
  2250. }
  2251. $options = static::$_fieldOptions[$field];
  2252. if (!is_null($key)) {
  2253. if (!isset($options[$key])) {
  2254. BDebug::debug('Invalid field options key: '.$field.'.'.$key);
  2255. return null;
  2256. }
  2257. return $options[$key];
  2258. }
  2259. return $options;
  2260. }
  2261. /**
  2262. * Model validation
  2263. *
  2264. * Validate provided data using model rules and parameter rules.
  2265. * Parameter rules will be merged with model rules and can override them.
  2266. * Event will be fired prior validation which will enable adding of rules or editing data
  2267. * Event will be fired if validation fails.
  2268. *
  2269. * @see BValidate::validateInput()
  2270. * @param array $data
  2271. * @param array $rules
  2272. * @return bool
  2273. */
  2274. public function validate($data, $rules = array())
  2275. {
  2276. $rules = array_merge($this->_validationRules, $rules);
  2277. BEvents::i()->fire($this->_origClass()."::validate:before", array("rules" => &$rules, "data" => &$data));
  2278. $valid = BValidate::i()->validateInput($data, $rules, 'address-form');
  2279. if (!$valid) {
  2280. BEvents::i()->fire($this->_origClass()."::validate:failed", array("rules" => &$rules, "data" => &$data));
  2281. }
  2282. return $valid;
  2283. }
  2284. public function __call($name, $args)
  2285. {
  2286. return BClassRegistry::i()->callMethod($this, $name, $args, static::$_origClass);
  2287. }
  2288. public static function __callStatic($name, $args)
  2289. {
  2290. return BClassRegistry::i()->callStaticMethod(get_called_class(), $name, $args, static::$_origClass);
  2291. }
  2292. }
  2293. /**
  2294. * Collection of models
  2295. *
  2296. * Should be (almost) drop in replacement for current straight arrays implementation
  2297. */
  2298. class BCollection extends BData
  2299. {
  2300. protected $_orm;
  2301. public function setOrm($orm)
  2302. {
  2303. $this->_orm = $orm;
  2304. return $this;
  2305. }
  2306. public function load($assoc = false)
  2307. {
  2308. $method = $assoc ? 'find_many_assoc' : 'find_many';
  2309. $this->_data = $this->_orm->$method();
  2310. return $this;
  2311. }
  2312. public function modelsAsArray()
  2313. {
  2314. return BDb::many_as_array($this->_data);
  2315. }
  2316. }
  2317. /**
  2318. * Basic user authentication and authorization class
  2319. */
  2320. class BModelUser extends BModel
  2321. {
  2322. protected static $_sessionUser;
  2323. protected static $_sessionUserNamespace = 'user';
  2324. public static function sessionUserId()
  2325. {
  2326. $userId = BSession::i()->data(static::$_sessionUserNamespace.'_id');
  2327. return $userId ? $userId : false;
  2328. }
  2329. public static function sessionUser($reset=false)
  2330. {
  2331. if (!static::isLoggedIn()) {
  2332. return false;
  2333. }
  2334. $session = BSession::i();
  2335. if ($reset || !static::$_sessionUser) {
  2336. static::$_sessionUser = static::load(static::sessionUserId());
  2337. }
  2338. return static::$_sessionUser;
  2339. }
  2340. public static function isLoggedIn()
  2341. {
  2342. return static::sessionUserId() ? true : false;
  2343. }
  2344. public function setPassword($password)
  2345. {
  2346. $this->password_hash = BUtil::fullSaltedHash($password);
  2347. return $this;
  2348. }
  2349. public function validatePassword($password)
  2350. {
  2351. return BUtil::validateSaltedHash($password, $this->password_hash);
  2352. }
  2353. public function onBeforeSave()
  2354. {
  2355. if (!parent::onBeforeSave()) return false;
  2356. if (!$this->create_at) $this->create_at = BDb::now();
  2357. $this->update_at = BDb::now();
  2358. if ($this->password) {
  2359. $this->password_hash = BUtil::fullSaltedHash($this->password);
  2360. }
  2361. return true;
  2362. }
  2363. static public function authenticate($username, $password)
  2364. {
  2365. /** @var FCom_Admin_Model_User */
  2366. $user = static::orm()->where(array('OR'=>array('username'=>$username, 'email'=>$username)))->find_one();
  2367. if (!$user || !$user->validatePassword($password)) {
  2368. return false;
  2369. }
  2370. return $user;
  2371. }
  2372. public function login()
  2373. {
  2374. $this->set('last_login', BDb::now())->save();
  2375. BSession::i()->data(array(
  2376. static::$_sessionUserNamespace.'_id' => $this->id,
  2377. static::$_sessionUserNamespace => serialize($this->as_array()),
  2378. ));
  2379. static::$_sessionUser = $this;
  2380. if ($this->locale) {
  2381. setlocale(LC_ALL, $this->locale);
  2382. }
  2383. if ($this->timezone) {
  2384. date_default_timezone_set($this->timezone);
  2385. }
  2386. BEvents::i()->fire(__METHOD__.':after', array('user'=>$this));
  2387. return $this;
  2388. }
  2389. public function authorize($role, $args=null)
  2390. {
  2391. if (is_null($args)) {
  2392. // check authorization
  2393. return true;
  2394. }
  2395. // set authorization
  2396. return $this;
  2397. }
  2398. public static function logout()
  2399. {
  2400. BSession::i()->data(static::$_sessionUserNamespace.'_id', false);
  2401. static::$_sessionUser = null;
  2402. BEvents::i()->fire(__METHOD__.':after', array('user' => $this));
  2403. }
  2404. public function recoverPassword($emailView='email/user-password-recover')
  2405. {
  2406. $this->set(array('password_nonce'=>BUtil::randomString(20)))->save();
  2407. if (($view = BLayout::i()->view($emailView))) {
  2408. $view->set('user', $this)->email();
  2409. }
  2410. return $this;
  2411. }
  2412. public function resetPassword($password, $emailView='email/user-password-reset')
  2413. {
  2414. $this->set(array('password_nonce'=>null))->setPassword($password)->save()->login();
  2415. if (($view = BLayout::i()->view($emailView))) {
  2416. $view->set('user', $this)->email();
  2417. }
  2418. return $this;
  2419. }
  2420. public static function signup($r)
  2421. {
  2422. $r = (array)$r;
  2423. if (empty($r['email'])
  2424. || empty($r['password']) || empty($r['password_confirm'])
  2425. || $r['password']!=$r['password_confirm']
  2426. ) {
  2427. throw new Exception('Incomplete or invalid form data.');
  2428. }
  2429. $r = BUtil::arrayMask($r, 'email,password');
  2430. $user = static::create($r)->save();
  2431. if (($view = BLayout::i()->view('email/user-new-user'))) {
  2432. $view->set('user', $user)->email();
  2433. }
  2434. if (($view = BLayout::i()->view('email/admin-new-user'))) {
  2435. $view->set('user', $user)->email();
  2436. }
  2437. return $user;
  2438. }
  2439. }
  2440. class BModelException extends BException
  2441. {
  2442. }