mapper.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. <?php
  2. /*
  3. Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
  4. This file is part of the Fat-Free Framework (http://fatfree.sf.net).
  5. THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
  6. ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  7. IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  8. PURPOSE.
  9. Please see the license.txt file for more information.
  10. */
  11. namespace DB\SQL;
  12. //! SQL data mapper
  13. class Mapper extends \DB\Cursor {
  14. //@{ Error messages
  15. const
  16. E_Adhoc='Unable to process ad hoc field %s';
  17. //@}
  18. protected
  19. //! PDO wrapper
  20. $db,
  21. //! Database engine
  22. $engine,
  23. //! SQL table
  24. $source,
  25. //! SQL table (quoted)
  26. $table,
  27. //! Last insert ID
  28. $_id,
  29. //! Defined fields
  30. $fields,
  31. //! Adhoc fields
  32. $adhoc=array();
  33. /**
  34. * Return database type
  35. * @return string
  36. **/
  37. function dbtype() {
  38. return 'SQL';
  39. }
  40. /**
  41. * Return TRUE if field is defined
  42. * @return bool
  43. * @param $key string
  44. **/
  45. function exists($key) {
  46. return array_key_exists($key,$this->fields+$this->adhoc);
  47. }
  48. /**
  49. * Assign value to field
  50. * @return scalar
  51. * @param $key string
  52. * @param $val scalar
  53. **/
  54. function set($key,$val) {
  55. if (array_key_exists($key,$this->fields)) {
  56. $val=is_null($val) && $this->fields[$key]['nullable']?
  57. NULL:$this->db->value($this->fields[$key]['pdo_type'],$val);
  58. if ($this->fields[$key]['value']!==$val ||
  59. $this->fields[$key]['default']!==$val && is_null($val))
  60. $this->fields[$key]['changed']=TRUE;
  61. return $this->fields[$key]['value']=$val;
  62. }
  63. // Parenthesize expression in case it's a subquery
  64. $this->adhoc[$key]=array('expr'=>'('.$val.')','value'=>NULL);
  65. return $val;
  66. }
  67. /**
  68. * Retrieve value of field
  69. * @return scalar
  70. * @param $key string
  71. **/
  72. function get($key) {
  73. if ($key=='_id')
  74. return $this->_id;
  75. elseif (array_key_exists($key,$this->fields))
  76. return $this->fields[$key]['value'];
  77. elseif (array_key_exists($key,$this->adhoc))
  78. return $this->adhoc[$key]['value'];
  79. user_error(sprintf(self::E_Field,$key));
  80. }
  81. /**
  82. * Clear value of field
  83. * @return NULL
  84. * @param $key string
  85. **/
  86. function clear($key) {
  87. if (array_key_exists($key,$this->adhoc))
  88. unset($this->adhoc[$key]);
  89. }
  90. /**
  91. * Get PHP type equivalent of PDO constant
  92. * @return string
  93. * @param $pdo string
  94. **/
  95. function type($pdo) {
  96. switch ($pdo) {
  97. case \PDO::PARAM_NULL:
  98. return 'unset';
  99. case \PDO::PARAM_INT:
  100. return 'int';
  101. case \PDO::PARAM_BOOL:
  102. return 'bool';
  103. case \PDO::PARAM_STR:
  104. return 'string';
  105. }
  106. }
  107. /**
  108. * Convert array to mapper object
  109. * @return object
  110. * @param $row array
  111. **/
  112. protected function factory($row) {
  113. $mapper=clone($this);
  114. $mapper->reset();
  115. foreach ($row as $key=>$val) {
  116. if (array_key_exists($key,$this->fields))
  117. $var='fields';
  118. elseif (array_key_exists($key,$this->adhoc))
  119. $var='adhoc';
  120. else
  121. continue;
  122. $mapper->{$var}[$key]['value']=$val;
  123. if ($var=='fields' && $mapper->{$var}[$key]['pkey'])
  124. $mapper->{$var}[$key]['previous']=$val;
  125. }
  126. $mapper->query=array(clone($mapper));
  127. if (isset($mapper->trigger['load']))
  128. \Base::instance()->call($mapper->trigger['load'],$mapper);
  129. return $mapper;
  130. }
  131. /**
  132. * Return fields of mapper object as an associative array
  133. * @return array
  134. * @param $obj object
  135. **/
  136. function cast($obj=NULL) {
  137. if (!$obj)
  138. $obj=$this;
  139. return array_map(
  140. function($row) {
  141. return $row['value'];
  142. },
  143. $obj->fields+$obj->adhoc
  144. );
  145. }
  146. /**
  147. * Build query string and execute
  148. * @return array
  149. * @param $fields string
  150. * @param $filter string|array
  151. * @param $options array
  152. * @param $ttl int
  153. **/
  154. function select($fields,$filter=NULL,array $options=NULL,$ttl=0) {
  155. if (!$options)
  156. $options=array();
  157. $options+=array(
  158. 'group'=>NULL,
  159. 'order'=>NULL,
  160. 'limit'=>0,
  161. 'offset'=>0
  162. );
  163. $sql='SELECT '.$fields.' FROM '.$this->table;
  164. $args=array();
  165. if ($filter) {
  166. if (is_array($filter)) {
  167. $args=isset($filter[1]) && is_array($filter[1])?
  168. $filter[1]:
  169. array_slice($filter,1,NULL,TRUE);
  170. $args=is_array($args)?$args:array(1=>$args);
  171. list($filter)=$filter;
  172. }
  173. $sql.=' WHERE '.$filter;
  174. }
  175. $db=$this->db;
  176. if ($options['group'])
  177. $sql.=' GROUP BY '.implode(',',array_map(
  178. function($str) use($db) {
  179. return preg_match('/^(\w+)(?:\h+HAVING|\h*(?:,|$))/i',
  180. $str,$parts)?
  181. ($db->quotekey($parts[1]).
  182. (isset($parts[2])?(' '.$parts[2]):'')):$str;
  183. },
  184. explode(',',$options['group'])));
  185. if ($options['order']) {
  186. $sql.=' ORDER BY '.implode(',',array_map(
  187. function($str) use($db) {
  188. return preg_match('/^(\w+)(?:\h+(ASC|DESC))?\h*(?:,|$)/i',
  189. $str,$parts)?
  190. ($db->quotekey($parts[1]).
  191. (isset($parts[2])?(' '.$parts[2]):'')):$str;
  192. },
  193. explode(',',$options['order'])));
  194. }
  195. if ($options['limit'])
  196. $sql.=' LIMIT '.(int)$options['limit'];
  197. if ($options['offset'])
  198. $sql.=' OFFSET '.(int)$options['offset'];
  199. $result=$this->db->exec($sql,$args,$ttl);
  200. $out=array();
  201. foreach ($result as &$row) {
  202. foreach ($row as $field=>&$val) {
  203. if (array_key_exists($field,$this->fields)) {
  204. if (!is_null($val) || !$this->fields[$field]['nullable'])
  205. $val=$this->db->value(
  206. $this->fields[$field]['pdo_type'],$val);
  207. }
  208. elseif (array_key_exists($field,$this->adhoc))
  209. $this->adhoc[$field]['value']=$val;
  210. unset($val);
  211. }
  212. $out[]=$this->factory($row);
  213. unset($row);
  214. }
  215. return $out;
  216. }
  217. /**
  218. * Return records that match criteria
  219. * @return array
  220. * @param $filter string|array
  221. * @param $options array
  222. * @param $ttl int
  223. **/
  224. function find($filter=NULL,array $options=NULL,$ttl=0) {
  225. if (!$options)
  226. $options=array();
  227. $options+=array(
  228. 'group'=>NULL,
  229. 'order'=>NULL,
  230. 'limit'=>0,
  231. 'offset'=>0
  232. );
  233. $adhoc='';
  234. foreach ($this->adhoc as $key=>$field)
  235. $adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key);
  236. return $this->select(($options['group']?:implode(',',
  237. array_map(array($this->db,'quotekey'),array_keys($this->fields)))).
  238. $adhoc,$filter,$options,$ttl);
  239. }
  240. /**
  241. * Count records that match criteria
  242. * @return int
  243. * @param $filter string|array
  244. * @param $ttl int
  245. **/
  246. function count($filter=NULL,$ttl=0) {
  247. $sql='SELECT COUNT(*) AS '.
  248. $this->db->quotekey('rows').' FROM '.$this->table;
  249. $args=array();
  250. if ($filter) {
  251. if (is_array($filter)) {
  252. $args=isset($filter[1]) && is_array($filter[1])?
  253. $filter[1]:
  254. array_slice($filter,1,NULL,TRUE);
  255. $args=is_array($args)?$args:array(1=>$args);
  256. list($filter)=$filter;
  257. }
  258. $sql.=' WHERE '.$filter;
  259. }
  260. $result=$this->db->exec($sql,$args,$ttl);
  261. return $result[0]['rows'];
  262. }
  263. /**
  264. * Return record at specified offset using same criteria as
  265. * previous load() call and make it active
  266. * @return array
  267. * @param $ofs int
  268. **/
  269. function skip($ofs=1) {
  270. $out=parent::skip($ofs);
  271. $dry=$this->dry();
  272. foreach ($this->fields as $key=>&$field) {
  273. $field['value']=$dry?NULL:$out->fields[$key]['value'];
  274. $field['changed']=FALSE;
  275. if ($field['pkey'])
  276. $field['previous']=$dry?NULL:$out->fields[$key]['value'];
  277. unset($field);
  278. }
  279. foreach ($this->adhoc as $key=>&$field) {
  280. $field['value']=$dry?NULL:$out->adhoc[$key]['value'];
  281. unset($field);
  282. }
  283. if (isset($this->trigger['load']))
  284. \Base::instance()->call($this->trigger['load'],$this);
  285. return $out;
  286. }
  287. /**
  288. * Insert new record
  289. * @return object
  290. **/
  291. function insert() {
  292. $args=array();
  293. $ctr=0;
  294. $fields='';
  295. $values='';
  296. $filter='';
  297. $pkeys=array();
  298. $nkeys=array();
  299. $ckeys=array();
  300. $inc=NULL;
  301. foreach ($this->fields as $key=>$field)
  302. if ($field['pkey'])
  303. $pkeys[$key]=$field['previous'];
  304. if (isset($this->trigger['beforeinsert']))
  305. \Base::instance()->call($this->trigger['beforeinsert'],
  306. array($this,$pkeys));
  307. foreach ($this->fields as $key=>&$field) {
  308. if ($field['pkey']) {
  309. $field['previous']=$field['value'];
  310. if (!$inc && $field['pdo_type']==\PDO::PARAM_INT &&
  311. empty($field['value']) && !$field['nullable'])
  312. $inc=$key;
  313. $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
  314. $nkeys[$ctr+1]=array($field['value'],$field['pdo_type']);
  315. }
  316. if ($field['changed'] && $key!=$inc) {
  317. $fields.=($ctr?',':'').$this->db->quotekey($key);
  318. $values.=($ctr?',':'').'?';
  319. $args[$ctr+1]=array($field['value'],$field['pdo_type']);
  320. $ctr++;
  321. $ckeys[]=$key;
  322. }
  323. $field['changed']=FALSE;
  324. unset($field);
  325. }
  326. if ($fields) {
  327. $this->db->exec(
  328. (preg_match('/mssql|dblib|sqlsrv/',$this->engine) &&
  329. array_intersect(array_keys($pkeys),$ckeys)?
  330. 'SET IDENTITY_INSERT '.$this->table.' ON;':'').
  331. 'INSERT INTO '.$this->table.' ('.$fields.') '.
  332. 'VALUES ('.$values.')',$args
  333. );
  334. $seq=NULL;
  335. if ($this->engine=='pgsql') {
  336. $names=array_keys($pkeys);
  337. $seq=$this->source.'_'.end($names).'_seq';
  338. }
  339. if ($this->engine!='oci')
  340. $this->_id=$this->db->lastinsertid($seq);
  341. // Reload to obtain default and auto-increment field values
  342. $this->load($inc?
  343. array($inc.'=?',$this->db->value(
  344. $this->fields[$inc]['pdo_type'],$this->_id)):
  345. array($filter,$nkeys));
  346. if (isset($this->trigger['afterinsert']))
  347. \Base::instance()->call($this->trigger['afterinsert'],
  348. array($this,$pkeys));
  349. }
  350. return $this;
  351. }
  352. /**
  353. * Update current record
  354. * @return object
  355. **/
  356. function update() {
  357. $args=array();
  358. $ctr=0;
  359. $pairs='';
  360. $filter='';
  361. $pkeys=array();
  362. foreach ($this->fields as $key=>$field)
  363. if ($field['pkey'])
  364. $pkeys[$key]=$field['previous'];
  365. if (isset($this->trigger['beforeupdate']))
  366. \Base::instance()->call($this->trigger['beforeupdate'],
  367. array($this,$pkeys));
  368. foreach ($this->fields as $key=>$field)
  369. if ($field['changed']) {
  370. $pairs.=($pairs?',':'').$this->db->quotekey($key).'=?';
  371. $args[$ctr+1]=array($field['value'],$field['pdo_type']);
  372. $ctr++;
  373. }
  374. foreach ($this->fields as $key=>$field)
  375. if ($field['pkey']) {
  376. $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
  377. $args[$ctr+1]=array($field['previous'],$field['pdo_type']);
  378. $ctr++;
  379. }
  380. if ($pairs) {
  381. $sql='UPDATE '.$this->table.' SET '.$pairs;
  382. if ($filter)
  383. $sql.=' WHERE '.$filter;
  384. $this->db->exec($sql,$args);
  385. if (isset($this->trigger['afterupdate']))
  386. \Base::instance()->call($this->trigger['afterupdate'],
  387. array($this,$pkeys));
  388. }
  389. return $this;
  390. }
  391. /**
  392. * Delete current record
  393. * @return int
  394. * @param $filter string|array
  395. **/
  396. function erase($filter=NULL) {
  397. if ($filter) {
  398. $args=array();
  399. if (is_array($filter)) {
  400. $args=isset($filter[1]) && is_array($filter[1])?
  401. $filter[1]:
  402. array_slice($filter,1,NULL,TRUE);
  403. $args=is_array($args)?$args:array(1=>$args);
  404. list($filter)=$filter;
  405. }
  406. return $this->db->
  407. exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args);
  408. }
  409. $args=array();
  410. $ctr=0;
  411. $filter='';
  412. $pkeys=array();
  413. foreach ($this->fields as $key=>&$field) {
  414. if ($field['pkey']) {
  415. $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
  416. $args[$ctr+1]=array($field['previous'],$field['pdo_type']);
  417. $pkeys[$key]=$field['previous'];
  418. $ctr++;
  419. }
  420. $field['value']=NULL;
  421. $field['changed']=(bool)$field['default'];
  422. if ($field['pkey'])
  423. $field['previous']=NULL;
  424. unset($field);
  425. }
  426. foreach ($this->adhoc as &$field) {
  427. $field['value']=NULL;
  428. unset($field);
  429. }
  430. parent::erase();
  431. $this->skip(0);
  432. if (isset($this->trigger['beforeerase']))
  433. \Base::instance()->call($this->trigger['beforeerase'],
  434. array($this,$pkeys));
  435. $out=$this->db->
  436. exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args);
  437. if (isset($this->trigger['aftererase']))
  438. \Base::instance()->call($this->trigger['aftererase'],
  439. array($this,$pkeys));
  440. return $out;
  441. }
  442. /**
  443. * Reset cursor
  444. * @return NULL
  445. **/
  446. function reset() {
  447. foreach ($this->fields as &$field) {
  448. $field['value']=NULL;
  449. $field['changed']=FALSE;
  450. if ($field['pkey'])
  451. $field['previous']=NULL;
  452. unset($field);
  453. }
  454. foreach ($this->adhoc as &$field) {
  455. $field['value']=NULL;
  456. unset($field);
  457. }
  458. parent::reset();
  459. }
  460. /**
  461. * Hydrate mapper object using hive array variable
  462. * @return NULL
  463. * @param $key string
  464. * @param $func callback
  465. **/
  466. function copyfrom($key,$func=NULL) {
  467. $var=\Base::instance()->get($key);
  468. if ($func)
  469. $var=call_user_func($func,$var);
  470. foreach ($var as $key=>$val)
  471. if (in_array($key,array_keys($this->fields))) {
  472. $field=&$this->fields[$key];
  473. if ($field['value']!==$val) {
  474. $field['value']=$val;
  475. $field['changed']=TRUE;
  476. }
  477. unset($field);
  478. }
  479. }
  480. /**
  481. * Populate hive array variable with mapper fields
  482. * @return NULL
  483. * @param $key string
  484. **/
  485. function copyto($key) {
  486. $var=&\Base::instance()->ref($key);
  487. foreach ($this->fields+$this->adhoc as $key=>$field)
  488. $var[$key]=$field['value'];
  489. }
  490. /**
  491. * Return schema
  492. * @return array
  493. **/
  494. function schema() {
  495. return $this->fields;
  496. }
  497. /**
  498. * Return field names
  499. * @return array
  500. * @param $adhoc bool
  501. **/
  502. function fields($adhoc=TRUE) {
  503. return array_keys($this->fields+($adhoc?$this->adhoc:array()));
  504. }
  505. /**
  506. * Instantiate class
  507. * @param $db object
  508. * @param $table string
  509. * @param $fields array|string
  510. * @param $ttl int
  511. **/
  512. function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) {
  513. $this->db=$db;
  514. $this->engine=$db->driver();
  515. if ($this->engine=='oci')
  516. $table=strtoupper($table);
  517. $this->source=$table;
  518. $this->table=$this->db->quotekey($table);
  519. $this->fields=$db->schema($table,$fields,$ttl);
  520. $this->reset();
  521. }
  522. }