mapper.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. <?php
  2. /*
  3. Copyright (c) 2009-2017 F3::Factory/Bong Cosca, All rights reserved.
  4. This file is part of the Fat-Free Framework (http://fatfreeframework.com).
  5. This is free software: you can redistribute it and/or modify it under the
  6. terms of the GNU General Public License as published by the Free Software
  7. Foundation, either version 3 of the License, or later.
  8. Fat-Free Framework is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. General Public License for more details.
  12. You should have received a copy of the GNU General Public License along
  13. with Fat-Free Framework. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. namespace DB\SQL;
  16. //! SQL data mapper
  17. class Mapper extends \DB\Cursor {
  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=[],
  33. //! Dynamic properties
  34. $props=[];
  35. /**
  36. * Return database type
  37. * @return string
  38. **/
  39. function dbtype() {
  40. return 'SQL';
  41. }
  42. /**
  43. * Return mapped table
  44. * @return string
  45. **/
  46. function table() {
  47. return $this->source;
  48. }
  49. /**
  50. * Return TRUE if any/specified field value has changed
  51. * @return bool
  52. * @param $key string
  53. **/
  54. function changed($key=NULL) {
  55. if (isset($key))
  56. return $this->fields[$key]['changed'];
  57. foreach($this->fields as $key=>$field)
  58. if ($field['changed'])
  59. return TRUE;
  60. return FALSE;
  61. }
  62. /**
  63. * Return TRUE if field is defined
  64. * @return bool
  65. * @param $key string
  66. **/
  67. function exists($key) {
  68. return array_key_exists($key,$this->fields+$this->adhoc);
  69. }
  70. /**
  71. * Assign value to field
  72. * @return scalar
  73. * @param $key string
  74. * @param $val scalar
  75. **/
  76. function set($key,$val) {
  77. if (array_key_exists($key,$this->fields)) {
  78. $val=is_null($val) && $this->fields[$key]['nullable']?
  79. NULL:$this->db->value($this->fields[$key]['pdo_type'],$val);
  80. if ($this->fields[$key]['initial']!==$val ||
  81. $this->fields[$key]['default']!==$val && is_null($val))
  82. $this->fields[$key]['changed']=TRUE;
  83. return $this->fields[$key]['value']=$val;
  84. }
  85. // Adjust result on existing expressions
  86. if (isset($this->adhoc[$key]))
  87. $this->adhoc[$key]['value']=$val;
  88. elseif (is_string($val))
  89. // Parenthesize expression in case it's a subquery
  90. $this->adhoc[$key]=['expr'=>'('.$val.')','value'=>NULL];
  91. else
  92. $this->props[$key]=$val;
  93. return $val;
  94. }
  95. /**
  96. * Retrieve value of field
  97. * @return scalar
  98. * @param $key string
  99. **/
  100. function &get($key) {
  101. if ($key=='_id')
  102. return $this->_id;
  103. elseif (array_key_exists($key,$this->fields))
  104. return $this->fields[$key]['value'];
  105. elseif (array_key_exists($key,$this->adhoc))
  106. return $this->adhoc[$key]['value'];
  107. elseif (array_key_exists($key,$this->props))
  108. return $this->props[$key];
  109. user_error(sprintf(self::E_Field,$key),E_USER_ERROR);
  110. }
  111. /**
  112. * Clear value of field
  113. * @return NULL
  114. * @param $key string
  115. **/
  116. function clear($key) {
  117. if (array_key_exists($key,$this->adhoc))
  118. unset($this->adhoc[$key]);
  119. else
  120. unset($this->props[$key]);
  121. }
  122. /**
  123. * Invoke dynamic method
  124. * @return mixed
  125. * @param $func string
  126. * @param $args array
  127. **/
  128. function __call($func,$args) {
  129. return call_user_func_array(
  130. (array_key_exists($func,$this->props)?
  131. $this->props[$func]:
  132. $this->$func),$args
  133. );
  134. }
  135. /**
  136. * Convert array to mapper object
  137. * @return static
  138. * @param $row array
  139. **/
  140. protected function factory($row) {
  141. $mapper=clone($this);
  142. $mapper->reset();
  143. foreach ($row as $key=>$val) {
  144. if (array_key_exists($key,$this->fields))
  145. $var='fields';
  146. elseif (array_key_exists($key,$this->adhoc))
  147. $var='adhoc';
  148. else
  149. continue;
  150. $mapper->{$var}[$key]['value']=$val;
  151. $mapper->{$var}[$key]['initial']=$val;
  152. if ($var=='fields' && $mapper->{$var}[$key]['pkey'])
  153. $mapper->{$var}[$key]['previous']=$val;
  154. }
  155. $mapper->query=[clone($mapper)];
  156. if (isset($mapper->trigger['load']))
  157. \Base::instance()->call($mapper->trigger['load'],$mapper);
  158. return $mapper;
  159. }
  160. /**
  161. * Return fields of mapper object as an associative array
  162. * @return array
  163. * @param $obj object
  164. **/
  165. function cast($obj=NULL) {
  166. if (!$obj)
  167. $obj=$this;
  168. return array_map(
  169. function($row) {
  170. return $row['value'];
  171. },
  172. $obj->fields+$obj->adhoc
  173. );
  174. }
  175. /**
  176. * Build query string and arguments
  177. * @return array
  178. * @param $fields string
  179. * @param $filter string|array
  180. * @param $options array
  181. **/
  182. function stringify($fields,$filter=NULL,array $options=NULL) {
  183. if (!$options)
  184. $options=[];
  185. $options+=[
  186. 'group'=>NULL,
  187. 'order'=>NULL,
  188. 'limit'=>0,
  189. 'offset'=>0
  190. ];
  191. $db=$this->db;
  192. $sql='SELECT '.$fields.' FROM '.$this->table;
  193. $args=[];
  194. if (is_array($filter)) {
  195. $args=isset($filter[1]) && is_array($filter[1])?
  196. $filter[1]:
  197. array_slice($filter,1,NULL,TRUE);
  198. $args=is_array($args)?$args:[1=>$args];
  199. list($filter)=$filter;
  200. }
  201. if ($filter)
  202. $sql.=' WHERE '.$filter;
  203. if ($options['group']) {
  204. $sql.=' GROUP BY '.implode(',',array_map(
  205. function($str) use($db) {
  206. return preg_replace_callback(
  207. '/\b(\w+[._\-\w]*)\h*(HAVING.+|$)/i',
  208. function($parts) use($db) {
  209. return $db->quotekey($parts[1]).
  210. (isset($parts[2])?(' '.$parts[2]):'');
  211. },
  212. $str
  213. );
  214. },
  215. explode(',',$options['group'])));
  216. }
  217. if ($options['order']) {
  218. $char=substr($db->quotekey(''),0,1);// quoting char
  219. $order=' ORDER BY '.(FALSE===strpos($options['order'],$char)?
  220. implode(',',array_map(function($str) use($db) {
  221. return preg_match('/^\h*(\w+[._\-\w]*)(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i',
  222. $str,$parts)?
  223. ($db->quotekey($parts[1]).
  224. (isset($parts[2])?(' '.$parts[2]):'')):$str;
  225. },explode(',',$options['order']))):
  226. $options['order']);
  227. }
  228. // SQL Server fixes
  229. if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) &&
  230. ($options['limit'] || $options['offset'])) {
  231. // order by pkey when no ordering option was given
  232. if (!$options['order'])
  233. foreach ($this->fields as $key=>$field)
  234. if ($field['pkey']) {
  235. $order=' ORDER BY '.$db->quotekey($key);
  236. break;
  237. }
  238. $ofs=$options['offset']?(int)$options['offset']:0;
  239. $lmt=$options['limit']?(int)$options['limit']:0;
  240. if (strncmp($db->version(),'11',2)>=0) {
  241. // SQL Server >= 2012
  242. $sql.=$order.' OFFSET '.$ofs.' ROWS';
  243. if ($lmt)
  244. $sql.=' FETCH NEXT '.$lmt.' ROWS ONLY';
  245. }
  246. else {
  247. // SQL Server 2008
  248. $sql=preg_replace('/SELECT/',
  249. 'SELECT '.
  250. ($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '.
  251. 'OVER ('.$order.') AS rnum,',$sql.$order,1);
  252. $sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs);
  253. }
  254. }
  255. else {
  256. if (isset($order))
  257. $sql.=$order;
  258. if ($options['limit'])
  259. $sql.=' LIMIT '.(int)$options['limit'];
  260. if ($options['offset'])
  261. $sql.=' OFFSET '.(int)$options['offset'];
  262. }
  263. return [$sql,$args];
  264. }
  265. /**
  266. * Build query string and execute
  267. * @return static[]
  268. * @param $fields string
  269. * @param $filter string|array
  270. * @param $options array
  271. * @param $ttl int|array
  272. **/
  273. function select($fields,$filter=NULL,array $options=NULL,$ttl=0) {
  274. list($sql,$args)=$this->stringify($fields,$filter,$options);
  275. $result=$this->db->exec($sql,$args,$ttl);
  276. $out=[];
  277. foreach ($result as &$row) {
  278. foreach ($row as $field=>&$val) {
  279. if (array_key_exists($field,$this->fields)) {
  280. if (!is_null($val) || !$this->fields[$field]['nullable'])
  281. $val=$this->db->value(
  282. $this->fields[$field]['pdo_type'],$val);
  283. }
  284. unset($val);
  285. }
  286. $out[]=$this->factory($row);
  287. unset($row);
  288. }
  289. return $out;
  290. }
  291. /**
  292. * Return records that match criteria
  293. * @return static[]
  294. * @param $filter string|array
  295. * @param $options array
  296. * @param $ttl int|array
  297. **/
  298. function find($filter=NULL,array $options=NULL,$ttl=0) {
  299. if (!$options)
  300. $options=[];
  301. $options+=[
  302. 'group'=>NULL,
  303. 'order'=>NULL,
  304. 'limit'=>0,
  305. 'offset'=>0
  306. ];
  307. $adhoc='';
  308. foreach ($this->adhoc as $key=>$field)
  309. $adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key);
  310. return $this->select(
  311. ($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)?
  312. $options['group']:
  313. implode(',',array_map([$this->db,'quotekey'],
  314. array_keys($this->fields)))).$adhoc,$filter,$options,$ttl);
  315. }
  316. /**
  317. * Count records that match criteria
  318. * @return int
  319. * @param $filter string|array
  320. * @param $options array
  321. * @param $ttl int|array
  322. **/
  323. function count($filter=NULL,array $options=NULL,$ttl=0) {
  324. $adhoc='';
  325. foreach ($this->adhoc as $key=>$field)
  326. $adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key);
  327. $fields='*'.$adhoc;
  328. if (preg_match('/mssql|dblib|sqlsrv/',$this->engine))
  329. $fields='TOP 100 PERCENT '.$fields;
  330. list($sql,$args)=$this->stringify($fields,$filter,$options);
  331. $sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '.
  332. 'FROM ('.$sql.') AS '.$this->db->quotekey('_temp');
  333. $result=$this->db->exec($sql,$args,$ttl);
  334. return (int)$result[0]['_rows'];
  335. }
  336. /**
  337. * Return record at specified offset using same criteria as
  338. * previous load() call and make it active
  339. * @return static
  340. * @param $ofs int
  341. **/
  342. function skip($ofs=1) {
  343. $out=parent::skip($ofs);
  344. $dry=$this->dry();
  345. foreach ($this->fields as $key=>&$field) {
  346. $field['value']=$dry?NULL:$out->fields[$key]['value'];
  347. $field['initial']=$field['value'];
  348. $field['changed']=FALSE;
  349. if ($field['pkey'])
  350. $field['previous']=$dry?NULL:$out->fields[$key]['value'];
  351. unset($field);
  352. }
  353. foreach ($this->adhoc as $key=>&$field) {
  354. $field['value']=$dry?NULL:$out->adhoc[$key]['value'];
  355. unset($field);
  356. }
  357. if (!$dry && isset($this->trigger['load']))
  358. \Base::instance()->call($this->trigger['load'],$this);
  359. return $out;
  360. }
  361. /**
  362. * Insert new record
  363. * @return static
  364. **/
  365. function insert() {
  366. $args=[];
  367. $actr=0;
  368. $nctr=0;
  369. $fields='';
  370. $values='';
  371. $filter='';
  372. $pkeys=[];
  373. $nkeys=[];
  374. $ckeys=[];
  375. $inc=NULL;
  376. foreach ($this->fields as $key=>$field)
  377. if ($field['pkey'])
  378. $pkeys[$key]=$field['previous'];
  379. if (isset($this->trigger['beforeinsert']) &&
  380. \Base::instance()->call($this->trigger['beforeinsert'],
  381. [$this,$pkeys])===FALSE)
  382. return $this;
  383. foreach ($this->fields as $key=>&$field) {
  384. if ($field['pkey']) {
  385. $field['previous']=$field['value'];
  386. if (!$inc && $field['pdo_type']==\PDO::PARAM_INT &&
  387. empty($field['value']) && !$field['nullable'])
  388. $inc=$key;
  389. $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
  390. $nkeys[$nctr+1]=[$field['value'],$field['pdo_type']];
  391. $nctr++;
  392. }
  393. if ($field['changed'] && $key!=$inc) {
  394. $fields.=($actr?',':'').$this->db->quotekey($key);
  395. $values.=($actr?',':'').'?';
  396. $args[$actr+1]=[$field['value'],$field['pdo_type']];
  397. $actr++;
  398. $ckeys[]=$key;
  399. }
  400. }
  401. if ($fields) {
  402. $add=$aik='';
  403. if ($this->engine=='pgsql' && !empty($pkeys)) {
  404. $names=array_keys($pkeys);
  405. $aik=end($names);
  406. $add=' RETURNING '.$this->db->quotekey($aik);
  407. }
  408. $lID=$this->db->exec(
  409. (preg_match('/mssql|dblib|sqlsrv/',$this->engine) &&
  410. array_intersect(array_keys($pkeys),$ckeys)?
  411. 'SET IDENTITY_INSERT '.$this->table.' ON;':'').
  412. 'INSERT INTO '.$this->table.' ('.$fields.') '.
  413. 'VALUES ('.$values.')'.$add,$args
  414. );
  415. if ($this->engine=='pgsql' && $lID && $aik)
  416. $this->_id=$lID[0][$aik];
  417. elseif ($this->engine!='oci')
  418. $this->_id=$this->db->lastinsertid();
  419. // Reload to obtain default and auto-increment field values
  420. if ($reload=(($inc && $this->_id) || $filter))
  421. $this->load($inc?
  422. [$inc.'=?',$this->db->value(
  423. $this->fields[$inc]['pdo_type'],$this->_id)]:
  424. [$filter,$nkeys]);
  425. if (isset($this->trigger['afterinsert']))
  426. \Base::instance()->call($this->trigger['afterinsert'],
  427. [$this,$pkeys]);
  428. // reset changed flag after calling afterinsert
  429. if (!$reload)
  430. foreach ($this->fields as $key=>&$field) {
  431. $field['changed']=FALSE;
  432. $field['initial']=$field['value'];
  433. unset($field);
  434. }
  435. }
  436. return $this;
  437. }
  438. /**
  439. * Update current record
  440. * @return static
  441. **/
  442. function update() {
  443. $args=[];
  444. $ctr=0;
  445. $pairs='';
  446. $filter='';
  447. $pkeys=[];
  448. foreach ($this->fields as $key=>$field)
  449. if ($field['pkey'])
  450. $pkeys[$key]=$field['previous'];
  451. if (isset($this->trigger['beforeupdate']) &&
  452. \Base::instance()->call($this->trigger['beforeupdate'],
  453. [$this,$pkeys])===FALSE)
  454. return $this;
  455. foreach ($this->fields as $key=>$field)
  456. if ($field['changed']) {
  457. $pairs.=($pairs?',':'').$this->db->quotekey($key).'=?';
  458. $args[++$ctr]=[$field['value'],$field['pdo_type']];
  459. }
  460. foreach ($this->fields as $key=>$field)
  461. if ($field['pkey']) {
  462. $filter.=($filter?' AND ':' WHERE ').
  463. $this->db->quotekey($key).'=?';
  464. $args[++$ctr]=[$field['previous'],$field['pdo_type']];
  465. }
  466. if ($pairs) {
  467. $sql='UPDATE '.$this->table.' SET '.$pairs.$filter;
  468. $this->db->exec($sql,$args);
  469. }
  470. if (isset($this->trigger['afterupdate']))
  471. \Base::instance()->call($this->trigger['afterupdate'],
  472. [$this,$pkeys]);
  473. // reset changed flag after calling afterupdate
  474. foreach ($this->fields as $key=>&$field) {
  475. $field['changed']=FALSE;
  476. $field['initial']=$field['value'];
  477. unset($field);
  478. }
  479. return $this;
  480. }
  481. /**
  482. * Delete current record
  483. * @return int
  484. * @param $quick bool
  485. * @param $filter string|array
  486. **/
  487. function erase($filter=NULL,$quick=TRUE) {
  488. if (isset($filter)) {
  489. if (!$quick) {
  490. $out=0;
  491. foreach ($this->find($filter) as $mapper)
  492. $out+=$mapper->erase();
  493. return $out;
  494. }
  495. $args=[];
  496. if (is_array($filter)) {
  497. $args=isset($filter[1]) && is_array($filter[1])?
  498. $filter[1]:
  499. array_slice($filter,1,NULL,TRUE);
  500. $args=is_array($args)?$args:[1=>$args];
  501. list($filter)=$filter;
  502. }
  503. return $this->db->
  504. exec('DELETE FROM '.$this->table.
  505. ($filter?' WHERE '.$filter:'').';',$args);
  506. }
  507. $args=[];
  508. $ctr=0;
  509. $filter='';
  510. $pkeys=[];
  511. foreach ($this->fields as $key=>&$field) {
  512. if ($field['pkey']) {
  513. $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
  514. $args[$ctr+1]=[$field['previous'],$field['pdo_type']];
  515. $pkeys[$key]=$field['previous'];
  516. $ctr++;
  517. }
  518. $field['value']=NULL;
  519. $field['changed']=(bool)$field['default'];
  520. if ($field['pkey'])
  521. $field['previous']=NULL;
  522. unset($field);
  523. }
  524. foreach ($this->adhoc as &$field) {
  525. $field['value']=NULL;
  526. unset($field);
  527. }
  528. parent::erase();
  529. if (isset($this->trigger['beforeerase']) &&
  530. \Base::instance()->call($this->trigger['beforeerase'],
  531. [$this,$pkeys])===FALSE)
  532. return 0;
  533. $out=$this->db->
  534. exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args);
  535. if (isset($this->trigger['aftererase']))
  536. \Base::instance()->call($this->trigger['aftererase'],
  537. [$this,$pkeys]);
  538. return $out;
  539. }
  540. /**
  541. * Reset cursor
  542. * @return NULL
  543. **/
  544. function reset() {
  545. foreach ($this->fields as &$field) {
  546. $field['value']=NULL;
  547. $field['initial']=NULL;
  548. $field['changed']=FALSE;
  549. if ($field['pkey'])
  550. $field['previous']=NULL;
  551. unset($field);
  552. }
  553. foreach ($this->adhoc as &$field) {
  554. $field['value']=NULL;
  555. unset($field);
  556. }
  557. parent::reset();
  558. }
  559. /**
  560. * Hydrate mapper object using hive array variable
  561. * @return NULL
  562. * @param $var array|string
  563. * @param $func callback
  564. **/
  565. function copyfrom($var,$func=NULL) {
  566. if (is_string($var))
  567. $var=\Base::instance()->$var;
  568. if ($func)
  569. $var=call_user_func($func,$var);
  570. foreach ($var as $key=>$val)
  571. if (in_array($key,array_keys($this->fields)))
  572. $this->set($key,$val);
  573. }
  574. /**
  575. * Populate hive array variable with mapper fields
  576. * @return NULL
  577. * @param $key string
  578. **/
  579. function copyto($key) {
  580. $var=&\Base::instance()->ref($key);
  581. foreach ($this->fields+$this->adhoc as $key=>$field)
  582. $var[$key]=$field['value'];
  583. }
  584. /**
  585. * Return schema and, if the first argument is provided, update it
  586. * @return array
  587. * @param $fields NULL|array
  588. **/
  589. function schema($fields=null) {
  590. if ($fields)
  591. $this->fields = $fields;
  592. return $this->fields;
  593. }
  594. /**
  595. * Return field names
  596. * @return array
  597. * @param $adhoc bool
  598. **/
  599. function fields($adhoc=TRUE) {
  600. return array_keys($this->fields+($adhoc?$this->adhoc:[]));
  601. }
  602. /**
  603. * Return TRUE if field is not nullable
  604. * @return bool
  605. * @param $field string
  606. **/
  607. function required($field) {
  608. return isset($this->fields[$field]) &&
  609. !$this->fields[$field]['nullable'];
  610. }
  611. /**
  612. * Retrieve external iterator for fields
  613. * @return object
  614. **/
  615. function getiterator() {
  616. return new \ArrayIterator($this->cast());
  617. }
  618. /**
  619. * Instantiate class
  620. * @param $db \DB\SQL
  621. * @param $table string
  622. * @param $fields array|string
  623. * @param $ttl int|array
  624. **/
  625. function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) {
  626. $this->db=$db;
  627. $this->engine=$db->driver();
  628. if ($this->engine=='oci')
  629. $table=strtoupper($table);
  630. $this->source=$table;
  631. $this->table=$this->db->quotekey($table);
  632. $this->fields=$db->schema($table,$fields,$ttl);
  633. $this->reset();
  634. }
  635. }