mapper.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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\Jig;
  12. //! Flat-file DB mapper
  13. class Mapper extends \DB\Cursor {
  14. protected
  15. //! Flat-file DB wrapper
  16. $db,
  17. //! Data file
  18. $file,
  19. //! Document identifier
  20. $id,
  21. //! Document contents
  22. $document=array();
  23. /**
  24. * Return database type
  25. * @return string
  26. **/
  27. function dbtype() {
  28. return 'Jig';
  29. }
  30. /**
  31. * Return TRUE if field is defined
  32. * @return bool
  33. * @param $key string
  34. **/
  35. function exists($key) {
  36. return array_key_exists($key,$this->document);
  37. }
  38. /**
  39. * Assign value to field
  40. * @return scalar|FALSE
  41. * @param $key string
  42. * @param $val scalar
  43. **/
  44. function set($key,$val) {
  45. return ($key=='_id')?FALSE:($this->document[$key]=$val);
  46. }
  47. /**
  48. * Retrieve value of field
  49. * @return scalar|FALSE
  50. * @param $key string
  51. **/
  52. function get($key) {
  53. if ($key=='_id')
  54. return $this->id;
  55. if (array_key_exists($key,$this->document))
  56. return $this->document[$key];
  57. user_error(sprintf(self::E_Field,$key));
  58. return FALSE;
  59. }
  60. /**
  61. * Delete field
  62. * @return NULL
  63. * @param $key string
  64. **/
  65. function clear($key) {
  66. if ($key!='_id')
  67. unset($this->document[$key]);
  68. }
  69. /**
  70. * Convert array to mapper object
  71. * @return object
  72. * @param $id string
  73. * @param $row array
  74. **/
  75. protected function factory($id,$row) {
  76. $mapper=clone($this);
  77. $mapper->reset();
  78. $mapper->id=$id;
  79. foreach ($row as $field=>$val)
  80. $mapper->document[$field]=$val;
  81. $mapper->query=array(clone($mapper));
  82. if (isset($mapper->trigger['load']))
  83. \Base::instance()->call($mapper->trigger['load'],$mapper);
  84. return $mapper;
  85. }
  86. /**
  87. * Return fields of mapper object as an associative array
  88. * @return array
  89. * @param $obj object
  90. **/
  91. function cast($obj=NULL) {
  92. if (!$obj)
  93. $obj=$this;
  94. return $obj->document+array('_id'=>$this->id);
  95. }
  96. /**
  97. * Convert tokens in string expression to variable names
  98. * @return string
  99. * @param $str string
  100. **/
  101. function token($str) {
  102. $self=$this;
  103. $str=preg_replace_callback(
  104. '/(?<!\w)@(\w(?:[\w\.\[\]])*)/',
  105. function($token) use($self) {
  106. // Convert from JS dot notation to PHP array notation
  107. return '$'.preg_replace_callback(
  108. '/(\.\w+)|\[((?:[^\[\]]*|(?R))*)\]/',
  109. function($expr) use($self) {
  110. $fw=\Base::instance();
  111. return
  112. '['.
  113. ($expr[1]?
  114. $fw->stringify(substr($expr[1],1)):
  115. (preg_match('/^\w+/',
  116. $mix=$self->token($expr[2]))?
  117. $fw->stringify($mix):
  118. $mix)).
  119. ']';
  120. },
  121. $token[1]
  122. );
  123. },
  124. $str
  125. );
  126. return trim($str);
  127. }
  128. /**
  129. * Return records that match criteria
  130. * @return array|FALSE
  131. * @param $filter array
  132. * @param $options array
  133. * @param $ttl int
  134. * @param $log bool
  135. **/
  136. function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) {
  137. if (!$options)
  138. $options=array();
  139. $options+=array(
  140. 'order'=>NULL,
  141. 'limit'=>0,
  142. 'offset'=>0
  143. );
  144. $fw=\Base::instance();
  145. $cache=\Cache::instance();
  146. $db=$this->db;
  147. $now=microtime(TRUE);
  148. $data=array();
  149. if (!$fw->get('CACHE') || !$ttl || !($cached=$cache->exists(
  150. $hash=$fw->hash($this->db->dir().
  151. $fw->stringify(array($filter,$options))).'.jig',$data)) ||
  152. $cached[0]+$ttl<microtime(TRUE)) {
  153. $data=$db->read($this->file);
  154. if (is_null($data))
  155. return FALSE;
  156. foreach ($data as $id=>&$doc) {
  157. $doc['_id']=$id;
  158. unset($doc);
  159. }
  160. if ($filter) {
  161. if (!is_array($filter))
  162. return FALSE;
  163. // Normalize equality operator
  164. $expr=preg_replace('/(?<=[^<>!=])=(?!=)/','==',$filter[0]);
  165. // Prepare query arguments
  166. $args=isset($filter[1]) && is_array($filter[1])?
  167. $filter[1]:
  168. array_slice($filter,1,NULL,TRUE);
  169. $args=is_array($args)?$args:array(1=>$args);
  170. $keys=$vals=array();
  171. $tokens=array_slice(
  172. token_get_all('<?php '.$this->token($expr)),1);
  173. $data=array_filter($data,
  174. function($_row) use($fw,$args,$tokens) {
  175. $_expr='';
  176. $ctr=0;
  177. $named=FALSE;
  178. foreach ($tokens as $token) {
  179. if (is_string($token))
  180. if ($token=='?') {
  181. // Positional
  182. $ctr++;
  183. $key=$ctr;
  184. }
  185. else {
  186. if ($token==':')
  187. $named=TRUE;
  188. else
  189. $_expr.=$token;
  190. continue;
  191. }
  192. elseif ($named &&
  193. token_name($token[0])=='T_STRING') {
  194. $key=':'.$token[1];
  195. $named=FALSE;
  196. }
  197. else {
  198. $_expr.=$token[1];
  199. continue;
  200. }
  201. $_expr.=$fw->stringify(
  202. is_string($args[$key])?
  203. addcslashes($args[$key],'\''):
  204. $args[$key]);
  205. }
  206. // Avoid conflict with user code
  207. unset($fw,$tokens,$args,$ctr,$token,$key,$named);
  208. extract($_row);
  209. // Evaluate pseudo-SQL expression
  210. return eval('return '.$_expr.';');
  211. }
  212. );
  213. }
  214. if (isset($options['order'])) {
  215. $cols=$fw->split($options['order']);
  216. uasort(
  217. $data,
  218. function($val1,$val2) use($cols) {
  219. foreach ($cols as $col) {
  220. $parts=explode(' ',$col,2);
  221. $order=empty($parts[1])?
  222. SORT_ASC:
  223. constant($parts[1]);
  224. $col=$parts[0];
  225. if (!array_key_exists($col,$val1))
  226. $val1[$col]=NULL;
  227. if (!array_key_exists($col,$val2))
  228. $val2[$col]=NULL;
  229. list($v1,$v2)=array($val1[$col],$val2[$col]);
  230. if ($out=strnatcmp($v1,$v2)*
  231. (($order==SORT_ASC)*2-1))
  232. return $out;
  233. }
  234. return 0;
  235. }
  236. );
  237. }
  238. $data=array_slice($data,
  239. $options['offset'],$options['limit']?:NULL,TRUE);
  240. if ($fw->get('CACHE') && $ttl)
  241. // Save to cache backend
  242. $cache->set($hash,$data,$ttl);
  243. }
  244. $out=array();
  245. foreach ($data as $id=>&$doc) {
  246. unset($doc['_id']);
  247. $out[]=$this->factory($id,$doc);
  248. unset($doc);
  249. }
  250. if ($log && isset($args)) {
  251. if ($filter)
  252. foreach ($args as $key=>$val) {
  253. $vals[]=$fw->stringify(is_array($val)?$val[0]:$val);
  254. $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
  255. }
  256. $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
  257. $this->file.' [find] '.
  258. ($filter?preg_replace($keys,$vals,$filter[0],1):''));
  259. }
  260. return $out;
  261. }
  262. /**
  263. * Count records that match criteria
  264. * @return int
  265. * @param $filter array
  266. * @param $ttl int
  267. **/
  268. function count($filter=NULL,$ttl=0) {
  269. $now=microtime(TRUE);
  270. $out=count($this->find($filter,NULL,$ttl,FALSE));
  271. $this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
  272. $this->file.' [count] '.($filter?json_encode($filter):''));
  273. return $out;
  274. }
  275. /**
  276. * Return record at specified offset using criteria of previous
  277. * load() call and make it active
  278. * @return array
  279. * @param $ofs int
  280. **/
  281. function skip($ofs=1) {
  282. $this->document=($out=parent::skip($ofs))?$out->document:array();
  283. $this->id=$out?$out->id:NULL;
  284. if ($this->document && isset($this->trigger['load']))
  285. \Base::instance()->call($this->trigger['load'],$this);
  286. return $out;
  287. }
  288. /**
  289. * Insert new record
  290. * @return array
  291. **/
  292. function insert() {
  293. if ($this->id)
  294. return $this->update();
  295. $db=$this->db;
  296. $now=microtime(TRUE);
  297. while (($id=uniqid(NULL,TRUE)) &&
  298. ($data=$db->read($this->file)) && isset($data[$id]) &&
  299. !connection_aborted())
  300. usleep(mt_rand(0,100));
  301. $this->id=$id;
  302. $data[$id]=$this->document;
  303. $pkey=array('_id'=>$this->id);
  304. if (isset($this->trigger['beforeinsert']))
  305. \Base::instance()->call($this->trigger['beforeinsert'],
  306. array($this,$pkey));
  307. $db->write($this->file,$data);
  308. $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
  309. $this->file.' [insert] '.json_encode($this->document));
  310. if (isset($this->trigger['afterinsert']))
  311. \Base::instance()->call($this->trigger['afterinsert'],
  312. array($this,$pkey));
  313. $this->load(array('@_id=?',$this->id));
  314. return $this->document;
  315. }
  316. /**
  317. * Update current record
  318. * @return array
  319. **/
  320. function update() {
  321. $db=$this->db;
  322. $now=microtime(TRUE);
  323. $data=$db->read($this->file);
  324. $data[$this->id]=$this->document;
  325. if (isset($this->trigger['beforeupdate']))
  326. \Base::instance()->call($this->trigger['beforeupdate'],
  327. array($this,array('_id'=>$this->id)));
  328. $db->write($this->file,$data);
  329. $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
  330. $this->file.' [update] '.json_encode($this->document));
  331. if (isset($this->trigger['afterupdate']))
  332. \Base::instance()->call($this->trigger['afterupdate'],
  333. array($this,array('_id'=>$this->id)));
  334. return $this->document;
  335. }
  336. /**
  337. * Delete current record
  338. * @return bool
  339. * @param $filter array
  340. **/
  341. function erase($filter=NULL) {
  342. $db=$this->db;
  343. $now=microtime(TRUE);
  344. $data=$db->read($this->file);
  345. $pkey=array('_id'=>$this->id);
  346. if ($filter) {
  347. foreach ($this->find($filter,NULL,FALSE) as $mapper)
  348. if (!$mapper->erase())
  349. return FALSE;
  350. return TRUE;
  351. }
  352. elseif (isset($this->id)) {
  353. unset($data[$this->id]);
  354. parent::erase();
  355. $this->skip(0);
  356. }
  357. else
  358. return FALSE;
  359. if (isset($this->trigger['beforeerase']))
  360. \Base::instance()->call($this->trigger['beforeerase'],
  361. array($this,$pkey));
  362. $db->write($this->file,$data);
  363. if ($filter) {
  364. $args=isset($filter[1]) && is_array($filter[1])?
  365. $filter[1]:
  366. array_slice($filter,1,NULL,TRUE);
  367. $args=is_array($args)?$args:array(1=>$args);
  368. foreach ($args as $key=>$val) {
  369. $vals[]=\Base::instance()->
  370. stringify(is_array($val)?$val[0]:$val);
  371. $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
  372. }
  373. }
  374. $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
  375. $this->file.' [erase] '.
  376. ($filter?preg_replace($keys,$vals,$filter[0],1):''));
  377. if (isset($this->trigger['aftererase']))
  378. \Base::instance()->call($this->trigger['aftererase'],
  379. array($this,$pkey));
  380. return TRUE;
  381. }
  382. /**
  383. * Reset cursor
  384. * @return NULL
  385. **/
  386. function reset() {
  387. $this->id=NULL;
  388. $this->document=array();
  389. parent::reset();
  390. }
  391. /**
  392. * Hydrate mapper object using hive array variable
  393. * @return NULL
  394. * @param $key string
  395. * @param $func callback
  396. **/
  397. function copyfrom($key,$func=NULL) {
  398. $var=\Base::instance()->get($key);
  399. if ($func)
  400. $var=call_user_func($func,$var);
  401. foreach ($var as $key=>$val)
  402. $this->document[$key]=$val;
  403. }
  404. /**
  405. * Populate hive array variable with mapper fields
  406. * @return NULL
  407. * @param $key string
  408. **/
  409. function copyto($key) {
  410. $var=&\Base::instance()->ref($key);
  411. foreach ($this->document as $key=>$field)
  412. $var[$key]=$field;
  413. }
  414. /**
  415. * Return field names
  416. * @return array
  417. **/
  418. function fields() {
  419. return array_keys($this->document);
  420. }
  421. /**
  422. * Instantiate class
  423. * @return void
  424. * @param $db object
  425. * @param $file string
  426. **/
  427. function __construct(\DB\Jig $db,$file) {
  428. $this->db=$db;
  429. $this->file=$file;
  430. $this->reset();
  431. }
  432. }