| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541 |
- <?php
- /*
- Copyright (c) 2009-2017 F3::Factory/Bong Cosca, All rights reserved.
- This file is part of the Fat-Free Framework (http://fatfreeframework.com).
- This is free software: you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation, either version 3 of the License, or later.
- Fat-Free Framework is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
- You should have received a copy of the GNU General Public License along
- with Fat-Free Framework. If not, see <http://www.gnu.org/licenses/>.
- */
- namespace DB\Jig;
- //! Flat-file DB mapper
- class Mapper extends \DB\Cursor {
- protected
- //! Flat-file DB wrapper
- $db,
- //! Data file
- $file,
- //! Document identifier
- $id,
- //! Document contents
- $document=[],
- //! field map-reduce handlers
- $_reduce;
- /**
- * Return database type
- * @return string
- **/
- function dbtype() {
- return 'Jig';
- }
- /**
- * Return TRUE if field is defined
- * @return bool
- * @param $key string
- **/
- function exists($key) {
- return array_key_exists($key,$this->document);
- }
- /**
- * Assign value to field
- * @return scalar|FALSE
- * @param $key string
- * @param $val scalar
- **/
- function set($key,$val) {
- return ($key=='_id')?FALSE:($this->document[$key]=$val);
- }
- /**
- * Retrieve value of field
- * @return scalar|FALSE
- * @param $key string
- **/
- function &get($key) {
- if ($key=='_id')
- return $this->id;
- if (array_key_exists($key,$this->document))
- return $this->document[$key];
- user_error(sprintf(self::E_Field,$key),E_USER_ERROR);
- }
- /**
- * Delete field
- * @return NULL
- * @param $key string
- **/
- function clear($key) {
- if ($key!='_id')
- unset($this->document[$key]);
- }
- /**
- * Convert array to mapper object
- * @return object
- * @param $id string
- * @param $row array
- **/
- function factory($id,$row) {
- $mapper=clone($this);
- $mapper->reset();
- $mapper->id=$id;
- foreach ($row as $field=>$val)
- $mapper->document[$field]=$val;
- $mapper->query=[clone($mapper)];
- if (isset($mapper->trigger['load']))
- \Base::instance()->call($mapper->trigger['load'],$mapper);
- return $mapper;
- }
- /**
- * Return fields of mapper object as an associative array
- * @return array
- * @param $obj object
- **/
- function cast($obj=NULL) {
- if (!$obj)
- $obj=$this;
- return $obj->document+['_id'=>$this->id];
- }
- /**
- * Convert tokens in string expression to variable names
- * @return string
- * @param $str string
- **/
- function token($str) {
- $str=preg_replace_callback(
- '/(?<!\w)@(\w(?:[\w\.\[\]])*)/',
- function($token) {
- // Convert from JS dot notation to PHP array notation
- return '$'.preg_replace_callback(
- '/(\.\w+)|\[((?:[^\[\]]*|(?R))*)\]/',
- function($expr) {
- $fw=\Base::instance();
- return
- '['.
- ($expr[1]?
- $fw->stringify(substr($expr[1],1)):
- (preg_match('/^\w+/',
- $mix=$this->token($expr[2]))?
- $fw->stringify($mix):
- $mix)).
- ']';
- },
- $token[1]
- );
- },
- $str
- );
- return trim($str);
- }
- /**
- * Return records that match criteria
- * @return static[]|FALSE
- * @param $filter array
- * @param $options array
- * @param $ttl int|array
- * @param $log bool
- **/
- function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) {
- if (!$options)
- $options=[];
- $options+=[
- 'order'=>NULL,
- 'limit'=>0,
- 'offset'=>0,
- 'group'=>NULL,
- ];
- $fw=\Base::instance();
- $cache=\Cache::instance();
- $db=$this->db;
- $now=microtime(TRUE);
- $data=[];
- $tag='';
- if (is_array($ttl))
- list($ttl,$tag)=$ttl;
- if (!$fw->CACHE || !$ttl || !($cached=$cache->exists(
- $hash=$fw->hash($this->db->dir().
- $fw->stringify([$filter,$options])).($tag?'.'.$tag:'').'.jig',$data)) ||
- $cached[0]+$ttl<microtime(TRUE)) {
- $data=$db->read($this->file);
- if (is_null($data))
- return FALSE;
- foreach ($data as $id=>&$doc) {
- $doc['_id']=$id;
- unset($doc);
- }
- if ($filter) {
- if (!is_array($filter))
- return FALSE;
- // Normalize equality operator
- $expr=preg_replace('/(?<=[^<>!=])=(?!=)/','==',$filter[0]);
- // Prepare query arguments
- $args=isset($filter[1]) && is_array($filter[1])?
- $filter[1]:
- array_slice($filter,1,NULL,TRUE);
- $args=is_array($args)?$args:[1=>$args];
- $keys=$vals=[];
- $tokens=array_slice(
- token_get_all('<?php '.$this->token($expr)),1);
- $data=array_filter($data,
- function($_row) use($fw,$args,$tokens) {
- $_expr='';
- $ctr=0;
- $named=FALSE;
- foreach ($tokens as $token) {
- if (is_string($token))
- if ($token=='?') {
- // Positional
- $ctr++;
- $key=$ctr;
- }
- else {
- if ($token==':')
- $named=TRUE;
- else
- $_expr.=$token;
- continue;
- }
- elseif ($named &&
- token_name($token[0])=='T_STRING') {
- $key=':'.$token[1];
- $named=FALSE;
- }
- else {
- $_expr.=$token[1];
- continue;
- }
- $_expr.=$fw->stringify(
- is_string($args[$key])?
- addcslashes($args[$key],'\''):
- $args[$key]);
- }
- // Avoid conflict with user code
- unset($fw,$tokens,$args,$ctr,$token,$key,$named);
- extract($_row);
- // Evaluate pseudo-SQL expression
- return eval('return '.$_expr.';');
- }
- );
- }
- if (isset($options['group'])) {
- $cols=array_reverse($fw->split($options['group']));
- // sort into groups
- $data=$this->sort($data,$options['group']);
- foreach($data as $i=>&$row) {
- if (!isset($prev)) {
- $prev=$row;
- $prev_i=$i;
- }
- $drop=false;
- foreach ($cols as $col)
- if ($prev_i!=$i && array_key_exists($col,$row) &&
- array_key_exists($col,$prev) && $row[$col]==$prev[$col])
- // reduce/modify
- $drop=!isset($this->_reduce[$col]) || call_user_func_array(
- $this->_reduce[$col][0],[&$prev,&$row])!==FALSE;
- elseif (isset($this->_reduce[$col])) {
- $null=null;
- // initial
- call_user_func_array($this->_reduce[$col][0],[&$row,&$null]);
- }
- if ($drop)
- unset($data[$i]);
- else {
- $prev=&$row;
- $prev_i=$i;
- }
- unset($row);
- }
- // finalize
- if ($this->_reduce[$col][1])
- foreach($data as $i=>&$row) {
- $row=call_user_func($this->_reduce[$col][1],$row);
- if (!$row)
- unset($data[$i]);
- unset($row);
- }
- }
- if (isset($options['order']))
- $data=$this->sort($data,$options['order']);
- $data=array_slice($data,
- $options['offset'],$options['limit']?:NULL,TRUE);
- if ($fw->CACHE && $ttl)
- // Save to cache backend
- $cache->set($hash,$data,$ttl);
- }
- $out=[];
- foreach ($data as $id=>&$doc) {
- unset($doc['_id']);
- $out[]=$this->factory($id,$doc);
- unset($doc);
- }
- if ($log && isset($args)) {
- if ($filter)
- foreach ($args as $key=>$val) {
- $vals[]=$fw->stringify(is_array($val)?$val[0]:$val);
- $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
- }
- $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
- $this->file.' [find] '.
- ($filter?preg_replace($keys,$vals,$filter[0],1):''));
- }
- return $out;
- }
- /**
- * Sort a collection
- * @param $data
- * @param $cond
- * @return mixed
- */
- protected function sort($data,$cond) {
- $cols=\Base::instance()->split($cond);
- uasort(
- $data,
- function($val1,$val2) use($cols) {
- foreach ($cols as $col) {
- $parts=explode(' ',$col,2);
- $order=empty($parts[1])?
- SORT_ASC:
- constant($parts[1]);
- $col=$parts[0];
- if (!array_key_exists($col,$val1))
- $val1[$col]=NULL;
- if (!array_key_exists($col,$val2))
- $val2[$col]=NULL;
- list($v1,$v2)=[$val1[$col],$val2[$col]];
- if ($out=strnatcmp($v1,$v2)*
- (($order==SORT_ASC)*2-1))
- return $out;
- }
- return 0;
- }
- );
- return $data;
- }
- /**
- * Add reduce handler for grouped fields
- * @param $key string
- * @param $handler callback
- * @param $finalize callback
- */
- function reduce($key,$handler,$finalize=null){
- $this->_reduce[$key]=[$handler,$finalize];
- }
- /**
- * Count records that match criteria
- * @return int
- * @param $filter array
- * @param $options array
- * @param $ttl int|array
- **/
- function count($filter=NULL,array $options=NULL,$ttl=0) {
- $now=microtime(TRUE);
- $out=count($this->find($filter,$options,$ttl,FALSE));
- $this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
- $this->file.' [count] '.($filter?json_encode($filter):''));
- return $out;
- }
- /**
- * Return record at specified offset using criteria of previous
- * load() call and make it active
- * @return array
- * @param $ofs int
- **/
- function skip($ofs=1) {
- $this->document=($out=parent::skip($ofs))?$out->document:[];
- $this->id=$out?$out->id:NULL;
- if ($this->document && isset($this->trigger['load']))
- \Base::instance()->call($this->trigger['load'],$this);
- return $out;
- }
- /**
- * Insert new record
- * @return array
- **/
- function insert() {
- if ($this->id)
- return $this->update();
- $db=$this->db;
- $now=microtime(TRUE);
- while (($id=uniqid(NULL,TRUE)) &&
- ($data=&$db->read($this->file)) && isset($data[$id]) &&
- !connection_aborted())
- usleep(mt_rand(0,100));
- $this->id=$id;
- $pkey=['_id'=>$this->id];
- if (isset($this->trigger['beforeinsert']) &&
- \Base::instance()->call($this->trigger['beforeinsert'],
- [$this,$pkey])===FALSE)
- return $this->document;
- $data[$id]=$this->document;
- $db->write($this->file,$data);
- $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
- $this->file.' [insert] '.json_encode($this->document));
- if (isset($this->trigger['afterinsert']))
- \Base::instance()->call($this->trigger['afterinsert'],
- [$this,$pkey]);
- $this->load(['@_id=?',$this->id]);
- return $this->document;
- }
- /**
- * Update current record
- * @return array
- **/
- function update() {
- $db=$this->db;
- $now=microtime(TRUE);
- $data=&$db->read($this->file);
- if (isset($this->trigger['beforeupdate']) &&
- \Base::instance()->call($this->trigger['beforeupdate'],
- [$this,['_id'=>$this->id]])===FALSE)
- return $this->document;
- $data[$this->id]=$this->document;
- $db->write($this->file,$data);
- $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
- $this->file.' [update] '.json_encode($this->document));
- if (isset($this->trigger['afterupdate']))
- \Base::instance()->call($this->trigger['afterupdate'],
- [$this,['_id'=>$this->id]]);
- return $this->document;
- }
- /**
- * Delete current record
- * @return bool
- * @param $filter array
- * @param $quick bool
- **/
- function erase($filter=NULL,$quick=FALSE) {
- $db=$this->db;
- $now=microtime(TRUE);
- $data=&$db->read($this->file);
- $pkey=['_id'=>$this->id];
- if ($filter) {
- foreach ($this->find($filter,NULL,FALSE) as $mapper)
- if (!$mapper->erase(null,$quick))
- return FALSE;
- return TRUE;
- }
- elseif (isset($this->id)) {
- unset($data[$this->id]);
- parent::erase();
- }
- else
- return FALSE;
- if (!$quick && isset($this->trigger['beforeerase']) &&
- \Base::instance()->call($this->trigger['beforeerase'],
- [$this,$pkey])===FALSE)
- return FALSE;
- $db->write($this->file,$data);
- if ($filter) {
- $args=isset($filter[1]) && is_array($filter[1])?
- $filter[1]:
- array_slice($filter,1,NULL,TRUE);
- $args=is_array($args)?$args:[1=>$args];
- foreach ($args as $key=>$val) {
- $vals[]=\Base::instance()->
- stringify(is_array($val)?$val[0]:$val);
- $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
- }
- }
- $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
- $this->file.' [erase] '.
- ($filter?preg_replace($keys,$vals,$filter[0],1):''));
- if (!$quick && isset($this->trigger['aftererase']))
- \Base::instance()->call($this->trigger['aftererase'],
- [$this,$pkey]);
- return TRUE;
- }
- /**
- * Reset cursor
- * @return NULL
- **/
- function reset() {
- $this->id=NULL;
- $this->document=[];
- parent::reset();
- }
- /**
- * Hydrate mapper object using hive array variable
- * @return NULL
- * @param $var array|string
- * @param $func callback
- **/
- function copyfrom($var,$func=NULL) {
- if (is_string($var))
- $var=\Base::instance()->$var;
- if ($func)
- $var=call_user_func($func,$var);
- foreach ($var as $key=>$val)
- $this->set($key,$val);
- }
- /**
- * Populate hive array variable with mapper fields
- * @return NULL
- * @param $key string
- **/
- function copyto($key) {
- $var=&\Base::instance()->ref($key);
- foreach ($this->document as $key=>$field)
- $var[$key]=$field;
- }
- /**
- * Return field names
- * @return array
- **/
- function fields() {
- return array_keys($this->document);
- }
- /**
- * Retrieve external iterator for fields
- * @return object
- **/
- function getiterator() {
- return new \ArrayIterator($this->cast());
- }
- /**
- * Instantiate class
- * @return void
- * @param $db object
- * @param $file string
- **/
- function __construct(\DB\Jig $db,$file) {
- $this->db=$db;
- $this->file=$file;
- $this->reset();
- }
- }
|