PageRenderTime 56ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/db.php

https://bitbucket.org/thejeshgn/kannu
PHP | 1016 lines | 660 code | 49 blank | 307 comment | 105 complexity | 71f7a8c3b201bdf5bbadf5befc608e5e MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /**
  3. SQL database plugin for the PHP Fat-Free Framework
  4. The contents of this file are subject to the terms of the GNU General
  5. Public License Version 3.0. You may not use this file except in
  6. compliance with the license. Any of the license terms and conditions
  7. can be waived if you get permission from the copyright holder.
  8. Copyright (c) 2009-2012 F3::Factory
  9. Bong Cosca <bong.cosca@yahoo.com>
  10. @package DB
  11. @version 2.0.13
  12. **/
  13. //! SQL data access layer
  14. class DB extends Base {
  15. //@{ Locale-specific error/exception messages
  16. const
  17. TEXT_ExecFail='Unable to execute prepared statement: %s',
  18. TEXT_DBEngine='Database engine is not supported',
  19. TEXT_Schema='Schema for %s table is not available';
  20. //@}
  21. public
  22. //! Exposed data object properties
  23. $dbname,$backend,$pdo,$result;
  24. private
  25. //! Connection parameters
  26. $dsn,$user,$pw,$opt,
  27. //! Transaction tracker
  28. $trans=FALSE,
  29. //! Auto-commit mode
  30. $auto=TRUE,
  31. //! Number of rows affected by query
  32. $rows=0;
  33. /**
  34. Force PDO instantiation
  35. @public
  36. **/
  37. function instantiate() {
  38. $this->pdo=new PDO($this->dsn,$this->user,$this->pw,$this->opt);
  39. }
  40. /**
  41. Begin SQL transaction
  42. @param $auto boolean
  43. @public
  44. **/
  45. function begin($auto=FALSE) {
  46. if (!$this->pdo)
  47. self::instantiate();
  48. $this->pdo->beginTransaction();
  49. $this->trans=TRUE;
  50. $this->auto=$auto;
  51. }
  52. /**
  53. Rollback SQL transaction
  54. @public
  55. **/
  56. function rollback() {
  57. if (!$this->pdo)
  58. self::instantiate();
  59. $this->pdo->rollback();
  60. $this->trans=FALSE;
  61. $this->auto=TRUE;
  62. }
  63. /**
  64. Commit SQL transaction
  65. @public
  66. **/
  67. function commit() {
  68. if (!$this->pdo)
  69. self::instantiate();
  70. $this->pdo->commit();
  71. $this->trans=FALSE;
  72. $this->auto=TRUE;
  73. }
  74. /**
  75. Process SQL statement(s)
  76. @return array
  77. @param $cmds mixed
  78. @param $args array
  79. @param $ttl int
  80. @param $assoc bool
  81. @public
  82. **/
  83. function exec($cmds,array $args=NULL,$ttl=0,$assoc=TRUE) {
  84. if (!$this->pdo)
  85. self::instantiate();
  86. $stats=&self::$vars['STATS'];
  87. if (!isset($stats[$this->dsn]))
  88. $stats[$this->dsn]=array(
  89. 'cache'=>array(),
  90. 'queries'=>array()
  91. );
  92. $batch=is_array($cmds);
  93. if ($batch) {
  94. if (!$this->trans && $this->auto)
  95. $this->begin(TRUE);
  96. if (is_null($args)) {
  97. $args=array();
  98. for ($i=0;$i<count($cmds);$i++)
  99. $args[]=NULL;
  100. }
  101. }
  102. else {
  103. $cmds=array($cmds);
  104. $args=array($args);
  105. }
  106. for ($i=0,$len=count($cmds);$i<$len;$i++) {
  107. list($cmd,$arg)=array($cmds[$i],$args[$i]);
  108. $hash='sql.'.self::hash($cmd.var_export($arg,TRUE));
  109. $cached=Cache::cached($hash);
  110. if ($ttl && $cached && $_SERVER['REQUEST_TIME']-$cached<$ttl) {
  111. // Gather cached queries for profiler
  112. if (!isset($stats[$this->dsn]['cache'][$cmd]))
  113. $stats[$this->dsn]['cache'][$cmd]=0;
  114. $stats[$this->dsn]['cache'][$cmd]++;
  115. $this->result=Cache::get($hash);
  116. }
  117. else {
  118. if (is_null($arg))
  119. $query=$this->pdo->query($cmd);
  120. else {
  121. $query=$this->pdo->prepare($cmd);
  122. if (is_object($query)) {
  123. foreach ($arg as $key=>$value)
  124. if (!(is_array($value)?
  125. $query->bindvalue($key,$value[0],$value[1]):
  126. $query->bindvalue($key,$value,
  127. $this->type($value))))
  128. break;
  129. $query->execute();
  130. }
  131. }
  132. // Check SQLSTATE
  133. foreach (array($this->pdo,$query) as $obj)
  134. if ($obj->errorCode()!=PDO::ERR_NONE) {
  135. if ($this->trans && $this->auto)
  136. $this->rollback();
  137. $error=$obj->errorinfo();
  138. trigger_error($error[2]);
  139. return FALSE;
  140. }
  141. if (preg_match(
  142. '/^\s*(?:SELECT|PRAGMA|SHOW|EXPLAIN)\s/i',$cmd)) {
  143. $this->result=$query->
  144. fetchall($assoc?PDO::FETCH_ASSOC:PDO::FETCH_COLUMN);
  145. $this->rows=$query->rowcount();
  146. }
  147. else
  148. $this->rows=$this->result=$query->rowCount();
  149. if ($ttl)
  150. Cache::set($hash,$this->result,$ttl);
  151. // Gather real queries for profiler
  152. if (!isset($stats[$this->dsn]['queries'][$cmd]))
  153. $stats[$this->dsn]['queries'][$cmd]=0;
  154. $stats[$this->dsn]['queries'][$cmd]++;
  155. }
  156. }
  157. if ($this->trans && $this->auto)
  158. $this->commit();
  159. return $this->result;
  160. }
  161. /**
  162. Return number of rows affected by latest query
  163. @return int
  164. **/
  165. function rows() {
  166. return $this->rows;
  167. }
  168. /**
  169. Return auto-detected PDO data type of specified value
  170. @return int
  171. @param $val mixed
  172. @public
  173. **/
  174. function type($val) {
  175. foreach (
  176. array(
  177. 'null'=>'NULL',
  178. 'bool'=>'BOOL',
  179. 'string'=>'STR',
  180. 'int'=>'INT',
  181. 'float'=>'STR'
  182. ) as $php=>$pdo)
  183. if (call_user_func('is_'.$php,$val))
  184. return constant('PDO::PARAM_'.$pdo);
  185. return PDO::PARAM_LOB;
  186. }
  187. /**
  188. Convenience method for direct SQL queries (static call)
  189. @return array
  190. @param $cmds mixed
  191. @param $args mixed
  192. @param $ttl int
  193. @param $db string
  194. @public
  195. **/
  196. static function sql($cmds,array $args=NULL,$ttl=0,$db='DB') {
  197. return self::$vars[$db]->exec($cmds,$args,$ttl);
  198. }
  199. /**
  200. Return schema of specified table
  201. @return array
  202. @param $table string
  203. @param $ttl int
  204. @public
  205. **/
  206. function schema($table,$ttl) {
  207. // Support these engines
  208. $cmd=array(
  209. 'sqlite2?'=>array(
  210. 'PRAGMA table_info('.$table.');',
  211. 'name','pk',1,'type','notnull',0,'dflt_value'),
  212. 'mysql'=>array(
  213. 'SHOW columns FROM `'.$this->dbname.'`.'.$table.';',
  214. 'Field','Key','PRI','Type','Null','YES','Default'),
  215. 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>array(
  216. 'SELECT '.
  217. 'c.column_name AS field,'.
  218. 'c.data_type AS type,'.
  219. 'c.is_nullable AS null,'.
  220. 'c.column_default AS default,'.
  221. 't.constraint_type AS pkey '.
  222. 'FROM information_schema.columns AS c '.
  223. 'LEFT OUTER JOIN '.
  224. 'information_schema.key_column_usage AS k '.
  225. 'ON '.
  226. 'c.table_name=k.table_name AND '.
  227. 'c.column_name=k.column_name '.
  228. ($this->dbname?
  229. ('AND '.
  230. (preg_match('/^pgsql$/',$this->backend)?
  231. 'c.table_catalog=k.table_catalog':
  232. 'c.table_schema=k.table_schema').' '):'').
  233. 'LEFT OUTER JOIN '.
  234. 'information_schema.table_constraints AS t '.
  235. 'ON '.
  236. 'k.table_name=t.table_name AND '.
  237. 'k.constraint_name=t.constraint_name '.
  238. ($this->dbname?
  239. ('AND '.
  240. (preg_match('/pgsql/',$this->backend)?
  241. 'k.table_catalog=t.table_catalog':
  242. 'k.table_schema=t.table_schema').' '):'').
  243. 'WHERE '.
  244. 'c.table_name=\''.$table.'\''.
  245. ($this->dbname?
  246. (' AND '.
  247. (preg_match('/pgsql/',$this->backend)?
  248. 'c.table_catalog':'c.table_schema').
  249. '=\''.$this->dbname.'\''):'').
  250. ';',
  251. 'field','pkey','PRIMARY KEY','type','null','YES','default'),
  252. 'ibm'=>array(
  253. 'SELECT DISTINCT '.
  254. 'c.colname AS field,'.
  255. 'c.typename AS type,'.
  256. 'c.nulls AS null,'.
  257. 'tc.type AS key'.
  258. 'c.default AS default'.
  259. 'FROM syscat.columns AS c '.
  260. 'LEFT JOIN '.
  261. '(syscat.keycoluse AS k '.
  262. 'JOIN syscat.tabconst AS tc '.
  263. 'ON '.
  264. 'k.tabschema=tc.tabschema AND '.
  265. 'k.tabname=tc.tabname AND '.
  266. 'tc.type=\'P\') '.
  267. 'ON '.
  268. 'c.tabschema=k.tabschema AND '.
  269. 'c.tabname=k.tabname AND '.
  270. 'c.colname=k.colname '.
  271. 'WHERE UPPER(c.tabname)=\''.strtoupper($table).'\';',
  272. 'field','key','P','type','null','Y','default'),
  273. );
  274. $match=FALSE;
  275. foreach ($cmd as $backend=>$val)
  276. if (preg_match('/'.$backend.'/',$this->backend)) {
  277. $match=TRUE;
  278. break;
  279. }
  280. if (!$match) {
  281. trigger_error(self::TEXT_DBEngine);
  282. return FALSE;
  283. }
  284. $result=$this->exec($val[0],NULL,$ttl);
  285. if (!$result) {
  286. trigger_error(sprintf(self::TEXT_Schema,$table));
  287. return FALSE;
  288. }
  289. return array(
  290. 'result'=>$result,
  291. 'field'=>$val[1],
  292. 'pkname'=>$val[2],
  293. 'pkval'=>$val[3],
  294. 'type'=>$val[4],
  295. 'nullname'=>$val[5],
  296. 'nullval'=>$val[6],
  297. 'default'=>$val[7]
  298. );
  299. }
  300. /**
  301. Custom session handler
  302. @param $table string
  303. @public
  304. **/
  305. function session($table='sessions') {
  306. $self=$this;
  307. session_set_save_handler(
  308. // Open
  309. function($path,$name) use($self,$table) {
  310. // Support these engines
  311. $cmd=array(
  312. 'sqlite2?'=>
  313. 'SELECT name FROM sqlite_master '.
  314. 'WHERE type=\'table\' AND name=\''.$table.'\';',
  315. 'mysql|mssql|sqlsrv|sybase|dblib|pgsql'=>
  316. 'SELECT table_name FROM information_schema.tables '.
  317. 'WHERE '.
  318. (preg_match('/pgsql/',$self->backend)?
  319. 'table_catalog':'table_schema').
  320. '=\''.$self->dbname.'\' AND '.
  321. 'table_name=\''.$table.'\''
  322. );
  323. foreach ($cmd as $backend=>$val)
  324. if (preg_match('/'.$backend.'/',$self->backend))
  325. break;
  326. $result=$self->exec($val,NULL);
  327. if (!$result)
  328. // Create SQL table
  329. $self->exec(
  330. 'CREATE TABLE '.
  331. (preg_match('/sqlite2?/',$self->backend)?
  332. '':($self->dbname.'.')).$table.' ('.
  333. 'id VARCHAR(40),'.
  334. 'data LONGTEXT,'.
  335. 'stamp INTEGER'.
  336. ');'
  337. );
  338. register_shutdown_function('session_commit');
  339. return TRUE;
  340. },
  341. // Close
  342. function() {
  343. return TRUE;
  344. },
  345. // Read
  346. function($id) use($table) {
  347. $axon=new Axon($table);
  348. $axon->load(array('id=:id',array(':id'=>$id)));
  349. return $axon->dry()?FALSE:$axon->data;
  350. },
  351. // Write
  352. function($id,$data) use($table) {
  353. $axon=new Axon($table);
  354. $axon->load(array('id=:id',array(':id'=>$id)));
  355. $axon->id=$id;
  356. $axon->data=$data;
  357. $axon->stamp=time();
  358. $axon->save();
  359. return TRUE;
  360. },
  361. // Delete
  362. function($id) use($table) {
  363. $axon=new Axon($table);
  364. $axon->erase(array('id=:id',array(':id'=>$id)));
  365. return TRUE;
  366. },
  367. // Cleanup
  368. function($max) use($table) {
  369. $axon=new Axon($table);
  370. $axon->erase('stamp+'.$max.'<'.time());
  371. return TRUE;
  372. }
  373. );
  374. }
  375. /**
  376. Class destructor
  377. @public
  378. **/
  379. function __destruct() {
  380. unset($this->pdo);
  381. }
  382. /**
  383. Class constructor
  384. @param $dsn string
  385. @param $user string
  386. @param $pw string
  387. @param $opt array
  388. @param $force boolean
  389. @public
  390. **/
  391. function __construct($dsn,$user=NULL,$pw=NULL,$opt=NULL,$force=FALSE) {
  392. if (!isset(self::$vars['MYSQL']))
  393. // Default MySQL character set
  394. self::$vars['MYSQL']='utf8';
  395. if (!$opt)
  396. $opt=array();
  397. // Append other default options
  398. $opt+=array(PDO::ATTR_EMULATE_PREPARES=>FALSE);
  399. if (in_array('mysql',pdo_drivers()) && preg_match('/^mysql:/',$dsn))
  400. $opt+=array(PDO::MYSQL_ATTR_INIT_COMMAND=>
  401. 'SET NAMES '.self::$vars['MYSQL']);
  402. list($this->dsn,$this->user,$this->pw,$this->opt)=
  403. array($this->resolve($dsn),$user,$pw,$opt);
  404. $this->backend=strstr($this->dsn,':',TRUE);
  405. preg_match('/dbname=([^;$]+)/',$this->dsn,$match);
  406. if ($match)
  407. $this->dbname=$match[1];
  408. if (!isset(self::$vars['DB']))
  409. self::$vars['DB']=$this;
  410. if ($force)
  411. $this->pdo=new PDO($this->dsn,$this->user,$this->pw,$this->opt);
  412. }
  413. }
  414. //! Axon ORM
  415. class Axon extends Base {
  416. //@{ Locale-specific error/exception messages
  417. const
  418. TEXT_AxonConnect='Undefined database',
  419. TEXT_AxonEmpty='Axon is empty',
  420. TEXT_AxonArray='Must be an array of Axon objects',
  421. TEXT_AxonNotMapped='The field %s does not exist',
  422. TEXT_AxonCantUndef='Cannot undefine an Axon-mapped field',
  423. TEXT_AxonCantUnset='Cannot unset an Axon-mapped field',
  424. TEXT_AxonConflict='Name conflict with Axon-mapped field',
  425. TEXT_AxonInvalid='Invalid virtual field expression',
  426. TEXT_AxonReadOnly='Virtual fields are read-only';
  427. //@}
  428. //@{
  429. //! Axon properties
  430. public
  431. $_id;
  432. private
  433. $db,$table,$pkeys,$fields,$types,$adhoc,$mod,$empty,$cond,$seq,$ofs;
  434. //@}
  435. /**
  436. Axon factory
  437. @return object
  438. @param $row array
  439. @public
  440. **/
  441. function factory($row) {
  442. $self=get_class($this);
  443. $axon=new $self($this->table,$this->db,FALSE);
  444. foreach ($row as $field=>$val) {
  445. if (method_exists($axon,'beforeLoad') &&
  446. $axon->beforeLoad()===FALSE)
  447. continue;
  448. if (array_key_exists($field,$this->fields)) {
  449. $axon->fields[$field]=$val;
  450. if ($this->pkeys &&
  451. array_key_exists($field,$this->pkeys))
  452. $axon->pkeys[$field]=$val;
  453. }
  454. else
  455. $axon->adhoc[$field]=array($this->adhoc[$field][0],$val);
  456. if ($axon->empty && $val)
  457. $axon->empty=FALSE;
  458. if (method_exists($axon,'afterLoad'))
  459. $axon->afterLoad();
  460. }
  461. return $axon;
  462. }
  463. /**
  464. Return current record contents as an array
  465. @return array
  466. @public
  467. **/
  468. function cast() {
  469. return $this->fields;
  470. }
  471. /**
  472. SQL select statement wrapper
  473. @return array
  474. @param $fields string
  475. @param $cond mixed
  476. @param $group string
  477. @param $seq string
  478. @param $limit int
  479. @param $ofs int
  480. @param $axon boolean
  481. @public
  482. **/
  483. function select(
  484. $fields=NULL,
  485. $cond=NULL,$group=NULL,$seq=NULL,$limit=0,$ofs=0,$axon=TRUE) {
  486. $rows=is_array($cond)?
  487. $this->db->exec(
  488. 'SELECT '.($fields?:'*').' FROM '.$this->table.
  489. ($cond?(' WHERE '.$cond[0]):'').
  490. ($group?(' GROUP BY '.$group):'').
  491. ($seq?(' ORDER BY '.$seq):'').
  492. (preg_match('/^mssql|sqlsrv|sybase|dblib$/',
  493. $this->db->backend)?
  494. (($ofs?(' OFFSET '.$ofs):'').
  495. ($limit?(' FETCH '.$limit.' ONLY'):'')):
  496. (($limit?(' LIMIT '.$limit):'').
  497. ($ofs?(' OFFSET '.$ofs):''))).';',
  498. $cond[1]
  499. ):
  500. $this->db->exec(
  501. 'SELECT '.($fields?:'*').' FROM '.$this->table.
  502. ($cond?(' WHERE '.$cond):'').
  503. ($group?(' GROUP BY '.$group):'').
  504. ($seq?(' ORDER BY '.$seq):'').
  505. (preg_match('/^mssql|sqlsrv|sybase|dblib$/',
  506. $this->db->backend)?
  507. (($ofs?(' OFFSET '.$ofs):'').
  508. ($limit?(' FETCH '.$limit.' ONLY'):'')):
  509. (($limit?(' LIMIT '.$limit):'').
  510. ($ofs?(' OFFSET '.$ofs):''))).';'
  511. );
  512. if ($axon)
  513. // Convert array elements to Axon objects
  514. foreach ($rows as &$row)
  515. $row=$this->factory($row);
  516. return $rows;
  517. }
  518. /**
  519. SQL select statement wrapper;
  520. Returns an array of associative arrays
  521. @return array
  522. @param $fields string
  523. @param $cond mixed
  524. @param $group string
  525. @param $seq string
  526. @param $limit int
  527. @param $ofs int
  528. @public
  529. **/
  530. function aselect(
  531. $fields=NULL,
  532. $cond=NULL,$group=NULL,$seq=NULL,$limit=0,$ofs=0) {
  533. return $this->select($fields,$cond,$group,$seq,$limit,$ofs,FALSE);
  534. }
  535. /**
  536. Return all records that match criteria
  537. @return array
  538. @param $cond mixed
  539. @param $seq string
  540. @param $limit int
  541. @param $ofs int
  542. @param $axon boolean
  543. @public
  544. **/
  545. function find($cond=NULL,$seq=NULL,$limit=0,$ofs=0,$axon=TRUE) {
  546. $adhoc='';
  547. if ($this->adhoc)
  548. foreach ($this->adhoc as $field=>$val)
  549. $adhoc.=','.$val[0].' AS '.$field;
  550. return $this->select('*'.$adhoc,$cond,NULL,$seq,$limit,$ofs,$axon);
  551. }
  552. /**
  553. Return all records that match criteria as an array of
  554. associative arrays
  555. @return array
  556. @param $cond mixed
  557. @param $seq string
  558. @param $limit int
  559. @param $ofs int
  560. @public
  561. **/
  562. function afind($cond=NULL,$seq=NULL,$limit=0,$ofs=0) {
  563. return $this->find($cond,$seq,$limit,$ofs,FALSE);
  564. }
  565. /**
  566. Retrieve first record that matches criteria
  567. @return array
  568. @param $cond mixed
  569. @param $seq string
  570. @param $ofs int
  571. @public
  572. **/
  573. function findone($cond=NULL,$seq=NULL,$ofs=0) {
  574. list($result)=$this->find($cond,$seq,1,$ofs)?:array(NULL);
  575. return $result;
  576. }
  577. /**
  578. Return the array equivalent of the object matching criteria
  579. @return array
  580. @param $cond mixed
  581. @param $seq string
  582. @param $ofs int
  583. @public
  584. **/
  585. function afindone($cond=NULL,$seq=NULL,$ofs=0) {
  586. list($result)=$this->afind($cond,$seq,1,$ofs)?:array(NULL);
  587. return $result;
  588. }
  589. /**
  590. Count records that match condition
  591. @return int
  592. @param $cond mixed
  593. @public
  594. **/
  595. function found($cond=NULL) {
  596. list($result)=$this->db->exec(
  597. 'SELECT COUNT(*) AS _found FROM '.$this->table.
  598. ($cond?(' WHERE '.(is_array($cond)?$cond[0]:$cond)):''),
  599. ($cond && is_array($cond)?$cond[1]:NULL)
  600. );
  601. return $result['_found'];
  602. }
  603. /**
  604. Dehydrate Axon
  605. @public
  606. **/
  607. function reset() {
  608. foreach (array_keys($this->fields) as $field)
  609. $this->fields[$field]=NULL;
  610. if ($this->pkeys)
  611. foreach (array_keys($this->pkeys) as $pkey)
  612. $this->pkeys[$pkey]=NULL;
  613. if ($this->adhoc)
  614. foreach (array_keys($this->adhoc) as $adhoc)
  615. $this->adhoc[$adhoc][1]=NULL;
  616. $this->empty=TRUE;
  617. $this->mod=NULL;
  618. $this->cond=NULL;
  619. $this->seq=NULL;
  620. $this->ofs=0;
  621. }
  622. /**
  623. Hydrate Axon with first record that matches criteria
  624. @return mixed
  625. @param $cond mixed
  626. @param $seq string
  627. @param $ofs int
  628. @public
  629. **/
  630. function load($cond=NULL,$seq=NULL,$ofs=0) {
  631. if ($ofs>-1) {
  632. $this->ofs=0;
  633. if ($axon=$this->findone($cond,$seq,$ofs)) {
  634. if (method_exists($this,'beforeLoad') &&
  635. $this->beforeLoad()===FALSE)
  636. return FALSE;
  637. // Hydrate Axon
  638. foreach ($axon->fields as $field=>$val) {
  639. $this->fields[$field]=$val;
  640. if ($this->pkeys &&
  641. array_key_exists($field,$this->pkeys))
  642. $this->pkeys[$field]=$val;
  643. }
  644. if ($axon->adhoc)
  645. foreach ($axon->adhoc as $field=>$val)
  646. $this->adhoc[$field][1]=$val[1];
  647. list($this->empty,$this->cond,$this->seq,$this->ofs)=
  648. array(FALSE,$cond,$seq,$ofs);
  649. if (method_exists($this,'afterLoad'))
  650. $this->afterLoad();
  651. return $this;
  652. }
  653. }
  654. $this->reset();
  655. return FALSE;
  656. }
  657. /**
  658. Hydrate Axon with nth record relative to current position
  659. @return mixed
  660. @param $ofs int
  661. @public
  662. **/
  663. function skip($ofs=1) {
  664. if ($this->dry()) {
  665. trigger_error(self::TEXT_AxonEmpty);
  666. return FALSE;
  667. }
  668. return $this->load($this->cond,$this->seq,$this->ofs+$ofs);
  669. }
  670. /**
  671. Return next record
  672. @return array
  673. @public
  674. **/
  675. function next() {
  676. return $this->skip();
  677. }
  678. /**
  679. Return previous record
  680. @return array
  681. @public
  682. **/
  683. function prev() {
  684. return $this->skip(-1);
  685. }
  686. /**
  687. Insert record/update database
  688. @param $id string
  689. @public
  690. **/
  691. function save($id=NULL) {
  692. if ($this->dry() ||
  693. method_exists($this,'beforeSave') &&
  694. $this->beforeSave()===FALSE)
  695. return;
  696. $new=TRUE;
  697. if ($this->pkeys)
  698. // If all primary keys are NULL, this is a new record
  699. foreach ($this->pkeys as $pkey)
  700. if (!is_null($pkey)) {
  701. $new=FALSE;
  702. break;
  703. }
  704. if ($new) {
  705. // Insert record
  706. $fields=$values='';
  707. $bind=array();
  708. foreach ($this->fields as $field=>$val) {
  709. $fields.=($fields?',':'').
  710. (preg_match('/^mysql$/',$this->db->backend)?
  711. ('`'.$field.'`'):$field);
  712. if (isset($this->mod[$field])) {
  713. $values.=($values?',':'').':'.$field;
  714. $bind[':'.$field]=array($val,$this->types[$field]);
  715. }
  716. }
  717. if ($bind)
  718. $this->db->exec(
  719. 'INSERT INTO '.$this->table.' ('.$fields.') '.
  720. 'VALUES ('.$values.');',$bind);
  721. $this->_id=$this->db->pdo->lastinsertid(
  722. preg_match('/pgsql/',$this->db->backend)?
  723. ($this->table.'_'.end($this->pkeys).'_seq'):
  724. NULL
  725. );
  726. if ($id)
  727. $this->pkeys[$id]=$this->_id;
  728. }
  729. elseif (!is_null($this->mod)) {
  730. // Update record
  731. $set=$cond='';
  732. foreach ($this->fields as $field=>$val)
  733. if (isset($this->mod[$field])) {
  734. $set.=($set?',':'').
  735. (preg_match('/^mysql$/',$this->db->backend)?
  736. ('`'.$field.'`'):$field).'=:'.$field;
  737. $bind[':'.$field]=array($val,$this->types[$field]);
  738. }
  739. // Use primary keys to find record
  740. if ($this->pkeys)
  741. foreach ($this->pkeys as $pkey=>$val) {
  742. $cond.=($cond?' AND ':'').$pkey.'=:c_'.$pkey;
  743. $bind[':c_'.$pkey]=array($val,$this->types[$pkey]);
  744. }
  745. if ($set)
  746. $this->db->exec(
  747. 'UPDATE '.$this->table.' SET '.$set.
  748. ($cond?(' WHERE '.$cond):'').';',$bind);
  749. }
  750. if ($this->pkeys)
  751. // Update primary keys with new values
  752. foreach (array_keys($this->pkeys) as $pkey)
  753. $this->pkeys[$pkey]=$this->fields[$pkey];
  754. $this->empty=FALSE;
  755. if (method_exists($this,'afterSave'))
  756. $this->afterSave();
  757. }
  758. /**
  759. Delete record/s
  760. @param $cond mixed
  761. @public
  762. **/
  763. function erase($cond=NULL) {
  764. if (method_exists($this,'beforeErase') &&
  765. $this->beforeErase()===FALSE)
  766. return;
  767. if (!$cond)
  768. $cond=$this->cond;
  769. if ($cond) {
  770. if (!is_array($cond))
  771. $cond=array($cond,NULL);
  772. $this->db->exec(
  773. 'DELETE FROM '.$this->table.' WHERE '.$cond[0],$cond[1]
  774. );
  775. }
  776. $this->reset();
  777. if (method_exists($this,'afterErase'))
  778. $this->afterErase();
  779. }
  780. /**
  781. Return TRUE if Axon is empty
  782. @return bool
  783. @public
  784. **/
  785. function dry() {
  786. return $this->empty;
  787. }
  788. /**
  789. Hydrate Axon with elements from array variable;
  790. Adhoc fields are not modified
  791. @param $name string
  792. @param $keys string
  793. @public
  794. **/
  795. function copyFrom($name,$keys=NULL) {
  796. $var=self::ref($name);
  797. $keys=is_null($keys)?array_keys($var):self::split($keys);
  798. foreach ($keys as $key)
  799. if (in_array($key,array_keys($var)) &&
  800. in_array($key,array_keys($this->fields))) {
  801. if ($this->fields[$key]!=$var[$key])
  802. $this->mod[$key]=TRUE;
  803. $this->fields[$key]=$var[$key];
  804. }
  805. $this->empty=FALSE;
  806. }
  807. /**
  808. Populate array variable with Axon properties
  809. @param $name string
  810. @param $keys string
  811. @public
  812. **/
  813. function copyTo($name,$keys=NULL) {
  814. if ($this->dry()) {
  815. trigger_error(self::TEXT_AxonEmpty);
  816. return;
  817. }
  818. $list=array_diff(preg_split('/[\|;,]/',$keys,0,
  819. PREG_SPLIT_NO_EMPTY),array(''));
  820. $keys=array_keys($this->fields);
  821. $adhoc=$this->adhoc?array_keys($this->adhoc):NULL;
  822. foreach ($adhoc?array_merge($keys,$adhoc):$keys as $key)
  823. if (empty($list) || in_array($key,$list)) {
  824. $var=&self::ref($name);
  825. if (in_array($key,array_keys($this->fields)))
  826. $var[$key]=$this->fields[$key];
  827. if ($this->adhoc &&
  828. in_array($key,array_keys($this->adhoc)))
  829. $var[$key]=$this->adhoc[$key];
  830. }
  831. }
  832. /**
  833. Synchronize Axon and SQL table structure
  834. @param $table string
  835. @param $db object
  836. @param $ttl int
  837. @public
  838. **/
  839. function sync($table,$db=NULL,$ttl=60) {
  840. if ($ttl===FALSE)
  841. return;
  842. if (!$db) {
  843. if (isset(self::$vars['DB']) && is_a(self::$vars['DB'],'DB'))
  844. $db=self::$vars['DB'];
  845. else {
  846. trigger_error(self::TEXT_AxonConnect);
  847. return;
  848. }
  849. }
  850. if (method_exists($this,'beforeSync') &&
  851. $this->beforeSync()===FALSE)
  852. return;
  853. // Initialize Axon
  854. list($this->db,$this->table)=array($db,$table);
  855. if ($schema=$db->schema($table,$ttl)) {
  856. // Populate properties
  857. foreach ($schema['result'] as $row) {
  858. $this->fields[$row[$schema['field']]]=NULL;
  859. if ($row[$schema['pkname']]==$schema['pkval'])
  860. // Save primary key
  861. $this->pkeys[$row[$schema['field']]]=NULL;
  862. $this->types[$row[$schema['field']]]=
  863. preg_match('/int|bool/i',$row[$schema['type']],$match)?
  864. constant('PDO::PARAM_'.strtoupper($match[0])):
  865. PDO::PARAM_STR;
  866. }
  867. $this->empty=TRUE;
  868. }
  869. if (method_exists($this,'afterSync'))
  870. $this->afterSync();
  871. }
  872. /**
  873. Create an adhoc field
  874. @param $field string
  875. @param $expr string
  876. @public
  877. **/
  878. function def($field,$expr) {
  879. if (array_key_exists($field,$this->fields)) {
  880. trigger_error(self::TEXT_AxonConflict);
  881. return;
  882. }
  883. $this->adhoc[$field]=array('('.$expr.')',NULL);
  884. }
  885. /**
  886. Destroy an adhoc field
  887. @param $field string
  888. @public
  889. **/
  890. function undef($field) {
  891. if (array_key_exists($field,$this->fields) || !self::isdef($field)) {
  892. trigger_error(sprintf(self::TEXT_AxonCantUndef,$field));
  893. return;
  894. }
  895. unset($this->adhoc[$field]);
  896. }
  897. /**
  898. Return TRUE if adhoc field exists
  899. @param $field string
  900. @public
  901. **/
  902. function isdef($field) {
  903. return $this->adhoc && array_key_exists($field,$this->adhoc);
  904. }
  905. /**
  906. Return value of mapped field
  907. @return mixed
  908. @param $field string
  909. @public
  910. **/
  911. function &__get($field) {
  912. if (array_key_exists($field,$this->fields))
  913. return $this->fields[$field];
  914. if (self::isdef($field))
  915. return $this->adhoc[$field][1];
  916. return self::$false;
  917. }
  918. /**
  919. Assign value to mapped field
  920. @return bool
  921. @param $field string
  922. @param $val mixed
  923. @public
  924. **/
  925. function __set($field,$val) {
  926. if (array_key_exists($field,$this->fields)) {
  927. if ($this->fields[$field]!==$val && !isset($this->mod[$field]))
  928. $this->mod[$field]=TRUE;
  929. $this->fields[$field]=$val;
  930. if (!is_null($val))
  931. $this->empty=FALSE;
  932. return TRUE;
  933. }
  934. if (self::isdef($field))
  935. trigger_error(self::TEXT_AxonReadOnly);
  936. return FALSE;
  937. }
  938. /**
  939. Trigger error in case a field is unset
  940. @param $field string
  941. @public
  942. **/
  943. function __unset($field) {
  944. trigger_error(self::TEXT_AxonCantUnset);
  945. }
  946. /**
  947. Return TRUE if mapped field is set
  948. @return bool
  949. @param $field string
  950. @public
  951. **/
  952. function __isset($field) {
  953. return array_key_exists($field,$this->fields) ||
  954. $this->adhoc && array_key_exists($field,$this->adhoc);
  955. }
  956. /**
  957. Class constructor
  958. @public
  959. **/
  960. function __construct() {
  961. // Execute mandatory sync method
  962. call_user_func_array(array($this,'sync'),func_get_args());
  963. }
  964. }