PageRenderTime 26ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/filedb.php

https://bitbucket.org/thejeshgn/php-bootstrap-boilerplate
PHP | 733 lines | 448 code | 43 blank | 242 comment | 57 complexity | 3eb8b097890c44dc34cba860ef0d850a MD5 | raw file
  1. <?php
  2. /**
  3. Simple Flat-file ORM 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 FileDB
  11. @version 2.0.13
  12. **/
  13. //! Flat-file data access layer
  14. class FileDB extends Base {
  15. //@{ Storage formats
  16. const
  17. FORMAT_Plain=0,
  18. FORMAT_Serialized=1,
  19. FORMAT_JSON=2,
  20. FORMAT_GZip=3;
  21. //@}
  22. public
  23. //! Exposed properties
  24. $path,$result;
  25. private
  26. //! Storage settings
  27. $format,
  28. //! Journal identifier
  29. $journal;
  30. /**
  31. Begin transaction
  32. @public
  33. **/
  34. function begin() {
  35. $this->journal=base_convert(microtime(TRUE)*100,10,16);
  36. }
  37. /**
  38. Rollback transaction
  39. @public
  40. **/
  41. function rollback() {
  42. if ($glob=glob($this->path.'*.jnl.'.$this->journal))
  43. foreach ($glob as $temp) {
  44. self::mutex(
  45. function() use($temp) {
  46. unlink($temp);
  47. },
  48. $temp
  49. );
  50. break;
  51. }
  52. $this->journal=NULL;
  53. }
  54. /**
  55. Commit transaction
  56. @public
  57. **/
  58. function commit() {
  59. if ($glob=glob($this->path.'*.jnl.'.$this->journal))
  60. foreach ($glob as $temp) {
  61. $file=preg_replace('/\.jnl\.'.$this->journal.'$/','',$temp);
  62. self::mutex(
  63. function() use($temp,$file) {
  64. @rename($temp,$file);
  65. },
  66. $temp,$file
  67. );
  68. break;
  69. }
  70. $this->journal=NULL;
  71. }
  72. /**
  73. Retrieve contents of flat-file
  74. @return mixed
  75. @param $file string
  76. @public
  77. **/
  78. function read($file) {
  79. $file=$this->path.$file;
  80. if (!is_file($file))
  81. return array();
  82. $text=self::getfile($file);
  83. $out='';
  84. switch ($this->format) {
  85. case self::FORMAT_GZip:
  86. $text=gzinflate($text);
  87. case self::FORMAT_Plain:
  88. if (ini_get('allow_url_fopen') &&
  89. ini_get('allow_url_include'))
  90. // Stream wrap
  91. $file='data:text/plain,'.urlencode($text);
  92. else {
  93. $file=self::$vars['TEMP'].$_SERVER['SERVER_NAME'].'.'.
  94. 'php.'.self::hash($file);
  95. self::putfile($file,$text);
  96. }
  97. $instance=new F3instance;
  98. $out=$instance->sandbox($file);
  99. break;
  100. case self::FORMAT_Serialized:
  101. $out=unserialize($text);
  102. break;
  103. case self::FORMAT_JSON:
  104. $out=json_decode($text,TRUE);
  105. }
  106. return $out;
  107. }
  108. /**
  109. Store PHP expression in flat-file
  110. @param $file string
  111. @param $expr mixed
  112. @public
  113. **/
  114. function write($file,$expr) {
  115. $file=$this->path.$file;
  116. $auto=FALSE;
  117. if (!$this->journal) {
  118. $auto=TRUE;
  119. $this->begin();
  120. if (is_file($file))
  121. copy($file,$file.'.jnl.'.$this->journal);
  122. }
  123. $file.='.jnl.'.$this->journal;
  124. if (!$expr)
  125. $expr=array();
  126. $out='<?php'."\n\n".'return '.self::stringify($expr).';'."\n";
  127. switch ($this->format) {
  128. case self::FORMAT_GZip:
  129. $out=gzdeflate($out);
  130. break;
  131. case self::FORMAT_Serialized:
  132. $out=serialize($expr);
  133. break;
  134. case self::FORMAT_JSON:
  135. $out=json_encode($expr);
  136. }
  137. if (self::putfile($file,$out)===FALSE)
  138. $this->rollback();
  139. elseif ($auto)
  140. $this->commit();
  141. }
  142. /**
  143. Convert database to another format
  144. @return bool
  145. @param $fmt int
  146. @public
  147. **/
  148. function convert($fmt) {
  149. $glob=glob($this->path.'*');
  150. if ($glob) {
  151. foreach ($glob as $file) {
  152. $file=str_replace($this->path,'',$file);
  153. $out=$this->read($file);
  154. switch ($fmt) {
  155. case self::FORMAT_GZip:
  156. $out=gzdeflate($out);
  157. break;
  158. case self::FORMAT_Serialized:
  159. $out=serialize($out);
  160. break;
  161. case self::FORMAT_JSON;
  162. $out=json_encode($out);
  163. }
  164. $this->format=$fmt;
  165. $this->write($file,$out);
  166. }
  167. return TRUE;
  168. }
  169. return FALSE;
  170. }
  171. /**
  172. Custom session handler
  173. @param $table string
  174. @public
  175. **/
  176. function session($table='sessions') {
  177. session_set_save_handler(
  178. // Open
  179. function($path,$name) {
  180. register_shutdown_function('session_commit');
  181. return TRUE;
  182. },
  183. // Close
  184. function() {
  185. return TRUE;
  186. },
  187. // Read
  188. function($id) use($table) {
  189. $jig=new Jig($table);
  190. $jig->load(array('id'=>$id));
  191. return $jig->dry()?FALSE:$jig->data;
  192. },
  193. // Write
  194. function($id,$data) use($table) {
  195. $jig=new Jig($table);
  196. $jig->load(array('id'=>$id));
  197. $jig->id=$id;
  198. $jig->data=$data;
  199. $jig->stamp=time();
  200. $jig->save();
  201. return TRUE;
  202. },
  203. // Delete
  204. function($id) use($table) {
  205. $jig=new Jig($table);
  206. $jig->erase(array('id'=>$id));
  207. return TRUE;
  208. },
  209. // Cleanup
  210. function($max) use($table) {
  211. $jig=new Jig($table);
  212. $jig->erase(
  213. array(
  214. '_PHP_'=>
  215. array(
  216. 'stamp'=>
  217. function($stamp) use($max) {
  218. return $stamp+$max<time();
  219. }
  220. )
  221. )
  222. );
  223. return TRUE;
  224. }
  225. );
  226. }
  227. /**
  228. Class constructor
  229. @param $path string
  230. @param $fmt int
  231. @public
  232. **/
  233. function __construct($path=NULL,$fmt=self::FORMAT_Plain) {
  234. $path=self::fixslashes(realpath(self::resolve($path)).'/');
  235. if (!is_dir($path))
  236. self::mkdir($path);
  237. list($this->path,$this->format)=array($path,$fmt);
  238. if (!isset(self::$vars['DB']))
  239. self::$vars['DB']=$this;
  240. }
  241. }
  242. //! Flat-file ORM
  243. class Jig extends Base {
  244. //@{ Locale-specific error/exception messages
  245. const
  246. TEXT_JigCriteria='Invalid criteria: %s',
  247. TEXT_JigCallback='Invalid callback: %s',
  248. TEXT_JigConnect='Undefined database',
  249. TEXT_JigEmpty='Jig is empty',
  250. TEXT_JigTable='Table %s does not exist',
  251. TEXT_JigField='Field %s does not exist';
  252. //@}
  253. //@{ Locale-specific error/exception messages
  254. const
  255. TEXT_Criteria='Invalid criteria: %s',
  256. TEXT_Callback='Invalid callback: %s';
  257. //@}
  258. //@{
  259. //! Jig properties
  260. public
  261. $_id;
  262. private
  263. $db,$table,$object,$mod,$cond,$seq,$ofs;
  264. //@}
  265. /**
  266. Jig factory
  267. @return object
  268. @param $obj array
  269. @public
  270. **/
  271. function factory($obj) {
  272. $self=get_class($this);
  273. $jig=new $self($this->table,$this->db);
  274. $jig->_id=$obj['_id'];
  275. unset($obj['_id']);
  276. foreach ($obj as $key=>$val)
  277. $jig->object[$key]=$val;
  278. return $jig;
  279. }
  280. /**
  281. Evaluate query criteria
  282. @return boolean
  283. @param $expr array
  284. @param $obj array
  285. @private
  286. **/
  287. private function check(array $expr,array $obj) {
  288. if (is_null($expr))
  289. return TRUE;
  290. if (is_array($expr)) {
  291. $result=TRUE;
  292. foreach ($expr as $field=>$cond) {
  293. if ($field=='_OR_') {
  294. if (!is_array($cond)) {
  295. trigger_error(
  296. sprintf(
  297. self::TEXT_JigCriteria,
  298. $this->stringify($cond)
  299. )
  300. );
  301. return FALSE;
  302. }
  303. foreach ($cond as $val)
  304. // Short circuit
  305. if ($this->check($val,$obj))
  306. return TRUE;
  307. return FALSE;
  308. }
  309. elseif ($field=='_PHP_') {
  310. list($key,$val)=array(key($cond),current($cond));
  311. if (!is_array($cond) || !is_callable($val)) {
  312. trigger_error(
  313. sprintf(
  314. self::TEXT_JigCallback,
  315. $this->stringify($val)
  316. )
  317. );
  318. return FALSE;
  319. }
  320. return isset($obj[$key])?
  321. call_user_func($val,$obj[$key]):TRUE;
  322. }
  323. elseif (!isset($obj[$field]) && !is_null($obj[$field]))
  324. $result=FALSE;
  325. elseif (is_array($cond)) {
  326. $map=array(
  327. '='=>'==',
  328. 'eq'=>'==',
  329. 'gt'=>'>',
  330. 'lt'=>'<',
  331. 'gte'=>'>=',
  332. 'lte'=>'<=',
  333. '<>'=>'!=',
  334. 'ne'=>'!='
  335. );
  336. foreach ($cond as $op=>$val)
  337. $result=($op=='_OR_' || $op=='_PHP_')?
  338. $this->check($val,$obj):
  339. ($op=='regex'?
  340. preg_match('/'.$val.'/s',$obj[$field]):
  341. eval(
  342. 'return $obj[$field]'.
  343. (isset($map[$op])?$map[$op]:$op).
  344. '(is_string($val)?'.
  345. 'self::resolve($val):$val);'));
  346. }
  347. else
  348. $result=($obj[$field]==(is_string($cond)?
  349. self::resolve($cond):$cond));
  350. if (!$result)
  351. break;
  352. }
  353. return $result;
  354. }
  355. return (bool)$expr;
  356. }
  357. /**
  358. Return current object contents as an array
  359. @return array
  360. @public
  361. **/
  362. function cast() {
  363. return array_merge(array('_id'=>$this->_id),$this->object);
  364. }
  365. /**
  366. Return an array of objects matching criteria
  367. @return array
  368. @param $cond array
  369. @param $seq array
  370. @param $limit mixed
  371. @param $ofs int
  372. @param $jig boolean
  373. @public
  374. **/
  375. function find(
  376. array $cond=NULL,array $seq=NULL,$limit=0,$ofs=0,$jig=TRUE) {
  377. $table=$this->db->read($this->table);
  378. $result=array();
  379. if ($table) {
  380. if (is_array($seq))
  381. foreach (array_reverse($seq,TRUE) as $key=>$sort)
  382. Matrix::sort($table,$key,$sort);
  383. foreach ($table as $key=>$obj) {
  384. $obj['_id']=$key;
  385. if (is_null($cond) || $this->check($cond,$obj))
  386. $result[]=$jig?$this->factory($obj):$obj;
  387. }
  388. $result=array_slice($result,$ofs,$limit?:NULL);
  389. }
  390. $this->db->result=$result;
  391. return $result;
  392. }
  393. /**
  394. Return an array of associative arrays matching criteria
  395. @return array
  396. @param $cond array
  397. @param $seq array
  398. @param $limit mixed
  399. @param $ofs int
  400. @public
  401. **/
  402. function afind(array $cond=NULL,array $seq=NULL,$limit=0,$ofs=0) {
  403. return $this->find($cond,$seq,$limit,$ofs,FALSE);
  404. }
  405. /**
  406. Return the first object that matches the specified criteria
  407. @return array
  408. @param $cond array
  409. @param $seq array
  410. @param $ofs int
  411. @public
  412. **/
  413. function findone(array $cond=NULL,array $seq=NULL,$ofs=0) {
  414. list($result)=$this->find($cond,$seq,1,$ofs)?:array(NULL);
  415. return $result;
  416. }
  417. /**
  418. Return the array equivalent of the object matching criteria
  419. @return array
  420. @param $cond array
  421. @param $seq array
  422. @param $ofs int
  423. @public
  424. **/
  425. function afindone(array $cond=NULL,array $seq=NULL,$ofs=0) {
  426. list($result)=$this->afind($cond,$seq,1,$ofs)?:array(NULL);
  427. return $result;
  428. }
  429. /**
  430. Count objects that match condition
  431. @return int
  432. @param $cond array
  433. @public
  434. **/
  435. function found(array $cond=NULL) {
  436. return count($this->find($cond));
  437. }
  438. /**
  439. Hydrate Jig with elements from framework array variable, keys of
  440. which will be identical to object properties
  441. @param $name string
  442. @public
  443. **/
  444. function copyFrom($name) {
  445. if (is_array($ref=self::ref($name))) {
  446. foreach ($ref as $key=>$val)
  447. $this->object[$key]=$val;
  448. $this->mod=TRUE;
  449. }
  450. }
  451. /**
  452. Populate framework array variable with Jig properties, keys of
  453. which will have names identical to object properties
  454. @param $name string
  455. @param $fields string
  456. @public
  457. **/
  458. function copyTo($name,$fields=NULL) {
  459. if ($this->dry()) {
  460. trigger_error(self::TEXT_JigEmpty);
  461. return;
  462. }
  463. if (is_string($fields))
  464. $list=preg_split('/[\|;,]/',$fields,0,PREG_SPLIT_NO_EMPTY);
  465. foreach (array_keys($this->object) as $field)
  466. if (!isset($list) || in_array($field,$list)) {
  467. $var=&self::ref($name);
  468. $var[$field]=$this->object[$field];
  469. }
  470. }
  471. /**
  472. Dehydrate Jig
  473. @public
  474. **/
  475. function reset() {
  476. // Dehydrate
  477. $this->_id=NULL;
  478. $this->object=NULL;
  479. $this->mod=NULL;
  480. $this->cond=NULL;
  481. $this->seq=NULL;
  482. $this->ofs=0;
  483. }
  484. /**
  485. Retrieve first object that satisfies criteria
  486. @return mixed
  487. @param $cond array
  488. @param $seq array
  489. @param $ofs int
  490. @public
  491. **/
  492. function load(array $cond=NULL,array $seq=NULL,$ofs=0) {
  493. if ($ofs>-1) {
  494. $this->ofs=0;
  495. if ($jig=$this->findone($cond,$seq,$ofs)) {
  496. if (method_exists($this,'beforeLoad') &&
  497. $this->beforeLoad()===FALSE)
  498. return FALSE;
  499. // Hydrate Jig
  500. $this->_id=$jig->_id;
  501. foreach ($jig->object as $key=>$val)
  502. $this->object[$key]=$val;
  503. list($this->cond,$this->seq,$this->ofs)=
  504. array($cond,$seq,$ofs);
  505. if (method_exists($this,'afterLoad'))
  506. $this->afterLoad();
  507. $this->mod=NULL;
  508. return $this;
  509. }
  510. }
  511. $this->reset();
  512. return FALSE;
  513. }
  514. /**
  515. Retrieve N-th object relative to current using the same criteria
  516. that hydrated Jig
  517. @return mixed
  518. @param $count int
  519. @public
  520. **/
  521. function skip($count=1) {
  522. if ($this->dry()) {
  523. trigger_error(self::TEXT_JigEmpty);
  524. return FALSE;
  525. }
  526. return $this->load($this->cond,$this->seq,$this->ofs+$count);
  527. }
  528. /**
  529. Return next record
  530. @return array
  531. @public
  532. **/
  533. function next() {
  534. return $this->skip();
  535. }
  536. /**
  537. Return previous record
  538. @return array
  539. @public
  540. **/
  541. function prev() {
  542. return $this->skip(-1);
  543. }
  544. /**
  545. Insert/update object
  546. @public
  547. **/
  548. function save() {
  549. if ($this->dry() ||
  550. method_exists($this,'beforeSave') &&
  551. $this->beforeSave()===FALSE)
  552. return;
  553. if ($this->mod) {
  554. // Object modified
  555. $table=$this->db->read($this->table);
  556. $obj=$this->object;
  557. if (!is_null($this->_id))
  558. // Update
  559. $id=$this->_id;
  560. else {
  561. // Insert with concurrency control
  562. while (($id=base_convert(microtime(TRUE)*100,10,16)) &&
  563. isset($table[$id])) {
  564. usleep(mt_rand(0,100));
  565. // Reload table
  566. $table=$this->db->read($this->table);
  567. }
  568. $this->_id=$id;
  569. }
  570. $table[$id]=$obj;
  571. // Save to file
  572. $this->db->write($this->table,$table);
  573. }
  574. if (method_exists($this,'afterSave'))
  575. $this->afterSave();
  576. }
  577. /**
  578. Delete object/s and reset Jig
  579. @param $cond array
  580. @param $force boolean
  581. @public
  582. **/
  583. function erase(array $cond=NULL,$force=FALSE) {
  584. if (method_exists($this,'beforeErase') &&
  585. $this->beforeErase()===FALSE)
  586. return;
  587. if (!$cond)
  588. $cond=$this->cond;
  589. if ($force || $cond) {
  590. $table=$this->db->read($this->table);
  591. foreach ($this->find($cond) as $found)
  592. unset($table[$found->_id]);
  593. // Save to file
  594. $this->db->write($this->table,$table);
  595. }
  596. $this->reset();
  597. if (method_exists($this,'afterErase'))
  598. $this->afterErase();
  599. }
  600. /**
  601. Return TRUE if Jig is NULL
  602. @return boolean
  603. @public
  604. **/
  605. function dry() {
  606. return is_null($this->object);
  607. }
  608. /**
  609. Synchronize Jig and underlying file
  610. @param $table string
  611. @param $db object
  612. @public
  613. **/
  614. function sync($table,$db=NULL) {
  615. if (!$db) {
  616. if (isset(self::$vars['DB']) &&
  617. is_a(self::$vars['DB'],'FileDB'))
  618. $db=self::$vars['DB'];
  619. else {
  620. trigger_error(self::TEXT_JigConnect);
  621. return;
  622. }
  623. }
  624. if (method_exists($this,'beforeSync') &&
  625. $this->beforeSync()===FALSE)
  626. return;
  627. // Initialize Jig
  628. list($this->db,$this->table)=array($db,$table);
  629. if (method_exists($this,'afterSync'))
  630. $this->afterSync();
  631. }
  632. /**
  633. Return value of Jig-mapped property
  634. @return boolean
  635. @param $name string
  636. @public
  637. **/
  638. function &__get($name) {
  639. return $this->object[$name];
  640. }
  641. /**
  642. Assign value to Jig-mapped property
  643. @return boolean
  644. @param $name string
  645. @param $val mixed
  646. @public
  647. **/
  648. function __set($name,$val) {
  649. if (!isset($this->object[$name]) || $this->object[$name]!=$val)
  650. $this->mod=TRUE;
  651. $this->object[$name]=$val;
  652. }
  653. /**
  654. Clear value of Jig-mapped property
  655. @return boolean
  656. @param $name string
  657. @public
  658. **/
  659. function __unset($name) {
  660. unset($this->object[$name]);
  661. $this->mod=TRUE;
  662. }
  663. /**
  664. Return TRUE if Jig-mapped property exists
  665. @return boolean
  666. @param $name string
  667. @public
  668. **/
  669. function __isset($name) {
  670. return array_key_exists($name,$this->object);
  671. }
  672. /**
  673. Display class name if conversion to string is attempted
  674. @public
  675. **/
  676. function __toString() {
  677. return get_class($this);
  678. }
  679. /**
  680. Class constructor
  681. @public
  682. **/
  683. function __construct() {
  684. // Execute mandatory sync method
  685. call_user_func_array(array($this,'sync'),func_get_args());
  686. }
  687. }