PageRenderTime 41ms CodeModel.GetById 10ms app.highlight 25ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/filedb.php

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