initial
This commit is contained in:
388
lib/db/cursor.php
Normal file
388
lib/db/cursor.php
Normal file
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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;
|
||||
|
||||
//! Simple cursor implementation
|
||||
abstract class Cursor extends \Magic implements \IteratorAggregate {
|
||||
|
||||
//@{ Error messages
|
||||
const
|
||||
E_Field='Undefined field %s';
|
||||
//@}
|
||||
|
||||
protected
|
||||
//! Query results
|
||||
$query=[],
|
||||
//! Current position
|
||||
$ptr=0,
|
||||
//! Event listeners
|
||||
$trigger=[];
|
||||
|
||||
/**
|
||||
* Return database type
|
||||
* @return string
|
||||
**/
|
||||
abstract function dbtype();
|
||||
|
||||
/**
|
||||
* Return field names
|
||||
* @return array
|
||||
**/
|
||||
abstract function fields();
|
||||
|
||||
/**
|
||||
* Return fields of mapper object as an associative array
|
||||
* @return array
|
||||
* @param $obj object
|
||||
**/
|
||||
abstract function cast($obj=NULL);
|
||||
|
||||
/**
|
||||
* Return records (array of mapper objects) that match criteria
|
||||
* @return array
|
||||
* @param $filter string|array
|
||||
* @param $options array
|
||||
* @param $ttl int
|
||||
**/
|
||||
abstract function find($filter=NULL,array $options=NULL,$ttl=0);
|
||||
|
||||
/**
|
||||
* Count records that match criteria
|
||||
* @return int
|
||||
* @param $filter array
|
||||
* @param $options array
|
||||
* @param $ttl int
|
||||
**/
|
||||
abstract function count($filter=NULL,array $options=NULL,$ttl=0);
|
||||
|
||||
/**
|
||||
* Insert new record
|
||||
* @return array
|
||||
**/
|
||||
abstract function insert();
|
||||
|
||||
/**
|
||||
* Update current record
|
||||
* @return array
|
||||
**/
|
||||
abstract function update();
|
||||
|
||||
/**
|
||||
* Hydrate mapper object using hive array variable
|
||||
* @return NULL
|
||||
* @param $var array|string
|
||||
* @param $func callback
|
||||
**/
|
||||
abstract function copyfrom($var,$func=NULL);
|
||||
|
||||
/**
|
||||
* Populate hive array variable with mapper fields
|
||||
* @return NULL
|
||||
* @param $key string
|
||||
**/
|
||||
abstract function copyto($key);
|
||||
|
||||
/**
|
||||
* Get cursor's equivalent external iterator
|
||||
* Causes a fatal error in PHP 5.3.5 if uncommented
|
||||
* return ArrayIterator
|
||||
**/
|
||||
abstract function getiterator();
|
||||
|
||||
|
||||
/**
|
||||
* Return TRUE if current cursor position is not mapped to any record
|
||||
* @return bool
|
||||
**/
|
||||
function dry() {
|
||||
return empty($this->query[$this->ptr]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return first record (mapper object) that matches criteria
|
||||
* @return static|FALSE
|
||||
* @param $filter string|array
|
||||
* @param $options array
|
||||
* @param $ttl int
|
||||
**/
|
||||
function findone($filter=NULL,array $options=NULL,$ttl=0) {
|
||||
if (!$options)
|
||||
$options=[];
|
||||
// Override limit
|
||||
$options['limit']=1;
|
||||
return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array containing subset of records matching criteria,
|
||||
* total number of records in superset, specified limit, number of
|
||||
* subsets available, and actual subset position
|
||||
* @return array
|
||||
* @param $pos int
|
||||
* @param $size int
|
||||
* @param $filter string|array
|
||||
* @param $options array
|
||||
* @param $ttl int
|
||||
* @param $bounce bool
|
||||
**/
|
||||
function paginate(
|
||||
$pos=0,$size=10,$filter=NULL,array $options=NULL,$ttl=0,$bounce=TRUE) {
|
||||
$total=$this->count($filter,$options,$ttl);
|
||||
$count=(int)ceil($total/$size);
|
||||
if ($bounce)
|
||||
$pos=max(0,min($pos,$count-1));
|
||||
return [
|
||||
'subset'=>($bounce || $pos<$count)?$this->find($filter,
|
||||
array_merge(
|
||||
$options?:[],
|
||||
['limit'=>$size,'offset'=>$pos*$size]
|
||||
),
|
||||
$ttl
|
||||
):[],
|
||||
'total'=>$total,
|
||||
'limit'=>$size,
|
||||
'count'=>$count,
|
||||
'pos'=>$bounce?($pos<$count?$pos:0):$pos
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to first record that matches criteria
|
||||
* @return array|FALSE
|
||||
* @param $filter string|array
|
||||
* @param $options array
|
||||
* @param $ttl int
|
||||
**/
|
||||
function load($filter=NULL,array $options=NULL,$ttl=0) {
|
||||
$this->reset();
|
||||
return ($this->query=$this->find($filter,$options,$ttl)) &&
|
||||
$this->skip(0)?$this->query[$this->ptr]:FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the count of records loaded
|
||||
* @return int
|
||||
**/
|
||||
function loaded() {
|
||||
return count($this->query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to first record in cursor
|
||||
* @return mixed
|
||||
**/
|
||||
function first() {
|
||||
return $this->skip(-$this->ptr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to last record in cursor
|
||||
* @return mixed
|
||||
**/
|
||||
function last() {
|
||||
return $this->skip(($ofs=count($this->query)-$this->ptr)?$ofs-1:0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to nth record relative to current cursor position
|
||||
* @return mixed
|
||||
* @param $ofs int
|
||||
**/
|
||||
function skip($ofs=1) {
|
||||
$this->ptr+=$ofs;
|
||||
return $this->ptr>-1 && $this->ptr<count($this->query)?
|
||||
$this->query[$this->ptr]:FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map next record
|
||||
* @return mixed
|
||||
**/
|
||||
function next() {
|
||||
return $this->skip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map previous record
|
||||
* @return mixed
|
||||
**/
|
||||
function prev() {
|
||||
return $this->skip(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether current iterator position is valid.
|
||||
*/
|
||||
function valid() {
|
||||
return !$this->dry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save mapped record
|
||||
* @return mixed
|
||||
**/
|
||||
function save() {
|
||||
return $this->query?$this->update():$this->insert();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete current record
|
||||
* @return int|bool
|
||||
**/
|
||||
function erase() {
|
||||
$this->query=array_slice($this->query,0,$this->ptr,TRUE)+
|
||||
array_slice($this->query,$this->ptr,NULL,TRUE);
|
||||
$this->skip(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define onload trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function onload($func) {
|
||||
return $this->trigger['load']=$func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define beforeinsert trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function beforeinsert($func) {
|
||||
return $this->trigger['beforeinsert']=$func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define afterinsert trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function afterinsert($func) {
|
||||
return $this->trigger['afterinsert']=$func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define oninsert trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function oninsert($func) {
|
||||
return $this->afterinsert($func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define beforeupdate trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function beforeupdate($func) {
|
||||
return $this->trigger['beforeupdate']=$func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define afterupdate trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function afterupdate($func) {
|
||||
return $this->trigger['afterupdate']=$func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define onupdate trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function onupdate($func) {
|
||||
return $this->afterupdate($func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define beforesave trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function beforesave($func) {
|
||||
$this->trigger['beforeinsert']=$func;
|
||||
$this->trigger['beforeupdate']=$func;
|
||||
return $func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define aftersave trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function aftersave($func) {
|
||||
$this->trigger['afterinsert']=$func;
|
||||
$this->trigger['afterupdate']=$func;
|
||||
return $func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define onsave trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function onsave($func) {
|
||||
return $this->aftersave($func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define beforeerase trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function beforeerase($func) {
|
||||
return $this->trigger['beforeerase']=$func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define aftererase trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function aftererase($func) {
|
||||
return $this->trigger['aftererase']=$func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define onerase trigger
|
||||
* @return callback
|
||||
* @param $func callback
|
||||
**/
|
||||
function onerase($func) {
|
||||
return $this->aftererase($func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cursor
|
||||
* @return NULL
|
||||
**/
|
||||
function reset() {
|
||||
$this->query=[];
|
||||
$this->ptr=0;
|
||||
}
|
||||
|
||||
}
|
||||
175
lib/db/jig.php
Normal file
175
lib/db/jig.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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;
|
||||
|
||||
//! In-memory/flat-file DB wrapper
|
||||
class Jig {
|
||||
|
||||
//@{ Storage formats
|
||||
const
|
||||
FORMAT_JSON=0,
|
||||
FORMAT_Serialized=1;
|
||||
//@}
|
||||
|
||||
protected
|
||||
//! UUID
|
||||
$uuid,
|
||||
//! Storage location
|
||||
$dir,
|
||||
//! Current storage format
|
||||
$format,
|
||||
//! Jig log
|
||||
$log,
|
||||
//! Memory-held data
|
||||
$data,
|
||||
//! lazy load/save files
|
||||
$lazy;
|
||||
|
||||
/**
|
||||
* Read data from memory/file
|
||||
* @return array
|
||||
* @param $file string
|
||||
**/
|
||||
function &read($file) {
|
||||
if (!$this->dir || !is_file($dst=$this->dir.$file)) {
|
||||
if (!isset($this->data[$file]))
|
||||
$this->data[$file]=[];
|
||||
return $this->data[$file];
|
||||
}
|
||||
if ($this->lazy && isset($this->data[$file]))
|
||||
return $this->data[$file];
|
||||
$fw=\Base::instance();
|
||||
$raw=$fw->read($dst);
|
||||
switch ($this->format) {
|
||||
case self::FORMAT_JSON:
|
||||
$data=json_decode($raw,TRUE);
|
||||
break;
|
||||
case self::FORMAT_Serialized:
|
||||
$data=$fw->unserialize($raw);
|
||||
break;
|
||||
}
|
||||
$this->data[$file] = $data;
|
||||
return $this->data[$file];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to memory/file
|
||||
* @return int
|
||||
* @param $file string
|
||||
* @param $data array
|
||||
**/
|
||||
function write($file,array $data=NULL) {
|
||||
if (!$this->dir || $this->lazy)
|
||||
return count($this->data[$file]=$data);
|
||||
$fw=\Base::instance();
|
||||
switch ($this->format) {
|
||||
case self::FORMAT_JSON:
|
||||
$out=json_encode($data,JSON_PRETTY_PRINT);
|
||||
break;
|
||||
case self::FORMAT_Serialized:
|
||||
$out=$fw->serialize($data);
|
||||
break;
|
||||
}
|
||||
return $fw->write($this->dir.$file,$out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return directory
|
||||
* @return string
|
||||
**/
|
||||
function dir() {
|
||||
return $this->dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return UUID
|
||||
* @return string
|
||||
**/
|
||||
function uuid() {
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return profiler results (or disable logging)
|
||||
* @param $flag bool
|
||||
* @return string
|
||||
**/
|
||||
function log($flag=TRUE) {
|
||||
if ($flag)
|
||||
return $this->log;
|
||||
$this->log=FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jot down log entry
|
||||
* @return NULL
|
||||
* @param $frame string
|
||||
**/
|
||||
function jot($frame) {
|
||||
if ($frame)
|
||||
$this->log.=date('r').' '.$frame.PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean storage
|
||||
* @return NULL
|
||||
**/
|
||||
function drop() {
|
||||
if ($this->lazy) // intentional
|
||||
$this->data=[];
|
||||
if (!$this->dir)
|
||||
$this->data=[];
|
||||
elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT))
|
||||
foreach ($glob as $file)
|
||||
@unlink($file);
|
||||
}
|
||||
|
||||
//! Prohibit cloning
|
||||
private function __clone() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate class
|
||||
* @param $dir string
|
||||
* @param $format int
|
||||
**/
|
||||
function __construct($dir=NULL,$format=self::FORMAT_JSON,$lazy=FALSE) {
|
||||
if ($dir && !is_dir($dir))
|
||||
mkdir($dir,\Base::MODE,TRUE);
|
||||
$this->uuid=\Base::instance()->hash($this->dir=$dir);
|
||||
$this->format=$format;
|
||||
$this->lazy=$lazy;
|
||||
}
|
||||
|
||||
/**
|
||||
* save file on destruction
|
||||
**/
|
||||
function __destruct() {
|
||||
if ($this->lazy) {
|
||||
$this->lazy = FALSE;
|
||||
foreach ($this->data?:[] as $file => $data)
|
||||
$this->write($file,$data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
541
lib/db/jig/mapper.php
Normal file
541
lib/db/jig/mapper.php
Normal file
@@ -0,0 +1,541 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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();
|
||||
}
|
||||
|
||||
}
|
||||
194
lib/db/jig/session.php
Normal file
194
lib/db/jig/session.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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;
|
||||
|
||||
//! Jig-managed session handler
|
||||
class Session extends Mapper {
|
||||
|
||||
protected
|
||||
//! Session ID
|
||||
$sid,
|
||||
//! Anti-CSRF token
|
||||
$_csrf,
|
||||
//! User agent
|
||||
$_agent,
|
||||
//! IP,
|
||||
$_ip,
|
||||
//! Suspect callback
|
||||
$onsuspect;
|
||||
|
||||
/**
|
||||
* Open session
|
||||
* @return TRUE
|
||||
* @param $path string
|
||||
* @param $name string
|
||||
**/
|
||||
function open($path,$name) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close session
|
||||
* @return TRUE
|
||||
**/
|
||||
function close() {
|
||||
$this->reset();
|
||||
$this->sid=NULL;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return session data in serialized format
|
||||
* @return string
|
||||
* @param $id string
|
||||
**/
|
||||
function read($id) {
|
||||
$this->load(['@session_id=?',$this->sid=$id]);
|
||||
if ($this->dry())
|
||||
return '';
|
||||
if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) {
|
||||
$fw=\Base::instance();
|
||||
if (!isset($this->onsuspect) ||
|
||||
$fw->call($this->onsuspect,[$this,$id])===FALSE) {
|
||||
// NB: `session_destroy` can't be called at that stage;
|
||||
// `session_start` not completed
|
||||
$this->destroy($id);
|
||||
$this->close();
|
||||
unset($fw->{'COOKIE.'.session_name()});
|
||||
$fw->error(403);
|
||||
}
|
||||
}
|
||||
return $this->get('data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write session data
|
||||
* @return TRUE
|
||||
* @param $id string
|
||||
* @param $data string
|
||||
**/
|
||||
function write($id,$data) {
|
||||
$this->set('session_id',$id);
|
||||
$this->set('data',$data);
|
||||
$this->set('ip',$this->_ip);
|
||||
$this->set('agent',$this->_agent);
|
||||
$this->set('stamp',time());
|
||||
$this->save();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy session
|
||||
* @return TRUE
|
||||
* @param $id string
|
||||
**/
|
||||
function destroy($id) {
|
||||
$this->erase(['@session_id=?',$id]);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collector
|
||||
* @return TRUE
|
||||
* @param $max int
|
||||
**/
|
||||
function cleanup($max) {
|
||||
$this->erase(['@stamp+?<?',$max,time()]);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return session id (if session has started)
|
||||
* @return string|NULL
|
||||
**/
|
||||
function sid() {
|
||||
return $this->sid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return anti-CSRF token
|
||||
* @return string
|
||||
**/
|
||||
function csrf() {
|
||||
return $this->_csrf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return IP address
|
||||
* @return string
|
||||
**/
|
||||
function ip() {
|
||||
return $this->_ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Unix timestamp
|
||||
* @return string|FALSE
|
||||
**/
|
||||
function stamp() {
|
||||
if (!$this->sid)
|
||||
session_start();
|
||||
return $this->dry()?FALSE:$this->get('stamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return HTTP user agent
|
||||
* @return string|FALSE
|
||||
**/
|
||||
function agent() {
|
||||
return $this->_agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate class
|
||||
* @param $db \DB\Jig
|
||||
* @param $file string
|
||||
* @param $onsuspect callback
|
||||
* @param $key string
|
||||
**/
|
||||
function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) {
|
||||
parent::__construct($db,$file);
|
||||
$this->onsuspect=$onsuspect;
|
||||
session_set_save_handler(
|
||||
[$this,'open'],
|
||||
[$this,'close'],
|
||||
[$this,'read'],
|
||||
[$this,'write'],
|
||||
[$this,'destroy'],
|
||||
[$this,'cleanup']
|
||||
);
|
||||
register_shutdown_function('session_commit');
|
||||
$fw=\Base::instance();
|
||||
$headers=$fw->HEADERS;
|
||||
$this->_csrf=$fw->hash($fw->SEED.
|
||||
extension_loaded('openssl')?
|
||||
implode(unpack('L',openssl_random_pseudo_bytes(4))):
|
||||
mt_rand()
|
||||
);
|
||||
if ($key)
|
||||
$fw->$key=$this->_csrf;
|
||||
$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';
|
||||
$this->_ip=$fw->IP;
|
||||
}
|
||||
|
||||
}
|
||||
145
lib/db/mongo.php
Normal file
145
lib/db/mongo.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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;
|
||||
|
||||
//! MongoDB wrapper
|
||||
class Mongo {
|
||||
|
||||
//@{
|
||||
const
|
||||
E_Profiler='MongoDB profiler is disabled';
|
||||
//@}
|
||||
|
||||
protected
|
||||
//! UUID
|
||||
$uuid,
|
||||
//! Data source name
|
||||
$dsn,
|
||||
//! MongoDB object
|
||||
$db,
|
||||
//! Legacy flag
|
||||
$legacy,
|
||||
//! MongoDB log
|
||||
$log;
|
||||
|
||||
/**
|
||||
* Return data source name
|
||||
* @return string
|
||||
**/
|
||||
function dsn() {
|
||||
return $this->dsn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return UUID
|
||||
* @return string
|
||||
**/
|
||||
function uuid() {
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return MongoDB profiler results (or disable logging)
|
||||
* @param $flag bool
|
||||
* @return string
|
||||
**/
|
||||
function log($flag=TRUE) {
|
||||
if ($flag) {
|
||||
$cursor=$this->db->selectcollection('system.profile')->find();
|
||||
foreach (iterator_to_array($cursor) as $frame)
|
||||
if (!preg_match('/\.system\..+$/',$frame['ns']))
|
||||
$this->log.=date('r',$this->legacy() ?
|
||||
$frame['ts']->sec : (round((string)$frame['ts'])/1000)).
|
||||
' ('.sprintf('%.1f',$frame['millis']).'ms) '.
|
||||
$frame['ns'].' ['.$frame['op'].'] '.
|
||||
(empty($frame['query'])?
|
||||
'':json_encode($frame['query'])).
|
||||
(empty($frame['command'])?
|
||||
'':json_encode($frame['command'])).
|
||||
PHP_EOL;
|
||||
} else {
|
||||
$this->log=FALSE;
|
||||
if ($this->legacy)
|
||||
$this->db->setprofilinglevel(-1);
|
||||
else
|
||||
$this->db->command(['profile'=>-1]);
|
||||
}
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept native call to re-enable profiler
|
||||
* @return int
|
||||
**/
|
||||
function drop() {
|
||||
$out=$this->db->drop();
|
||||
if ($this->log!==FALSE) {
|
||||
if ($this->legacy)
|
||||
$this->db->setprofilinglevel(2);
|
||||
else
|
||||
$this->db->command(['profile'=>2]);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect call to MongoDB object
|
||||
* @return mixed
|
||||
* @param $func string
|
||||
* @param $args array
|
||||
**/
|
||||
function __call($func,array $args) {
|
||||
return call_user_func_array([$this->db,$func],$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if legacy driver is loaded
|
||||
* @return bool
|
||||
**/
|
||||
function legacy() {
|
||||
return $this->legacy;
|
||||
}
|
||||
|
||||
//! Prohibit cloning
|
||||
private function __clone() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate class
|
||||
* @param $dsn string
|
||||
* @param $dbname string
|
||||
* @param $options array
|
||||
**/
|
||||
function __construct($dsn,$dbname,array $options=NULL) {
|
||||
$this->uuid=\Base::instance()->hash($this->dsn=$dsn);
|
||||
if ($this->legacy=class_exists('\MongoClient')) {
|
||||
$this->db=new \MongoDB(new \MongoClient($dsn,$options?:[]),$dbname);
|
||||
$this->db->setprofilinglevel(2);
|
||||
}
|
||||
else {
|
||||
$this->db=(new \MongoDB\Client($dsn,$options?:[]))->$dbname;
|
||||
$this->db->command(['profile'=>2]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
405
lib/db/mongo/mapper.php
Normal file
405
lib/db/mongo/mapper.php
Normal file
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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\Mongo;
|
||||
|
||||
//! MongoDB mapper
|
||||
class Mapper extends \DB\Cursor {
|
||||
|
||||
protected
|
||||
//! MongoDB wrapper
|
||||
$db,
|
||||
//! Legacy flag
|
||||
$legacy,
|
||||
//! Mongo collection
|
||||
$collection,
|
||||
//! Mongo document
|
||||
$document=[],
|
||||
//! Mongo cursor
|
||||
$cursor,
|
||||
//! Defined fields
|
||||
$fields;
|
||||
|
||||
/**
|
||||
* Return database type
|
||||
* @return string
|
||||
**/
|
||||
function dbtype() {
|
||||
return 'Mongo';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $this->document[$key]=$val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve value of field
|
||||
* @return scalar|FALSE
|
||||
* @param $key string
|
||||
**/
|
||||
function &get($key) {
|
||||
if ($this->exists($key))
|
||||
return $this->document[$key];
|
||||
user_error(sprintf(self::E_Field,$key),E_USER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete field
|
||||
* @return NULL
|
||||
* @param $key string
|
||||
**/
|
||||
function clear($key) {
|
||||
unset($this->document[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array to mapper object
|
||||
* @return static
|
||||
* @param $row array
|
||||
**/
|
||||
function factory($row) {
|
||||
$mapper=clone($this);
|
||||
$mapper->reset();
|
||||
foreach ($row as $key=>$val)
|
||||
$mapper->document[$key]=$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query and execute
|
||||
* @return static[]
|
||||
* @param $fields string
|
||||
* @param $filter array
|
||||
* @param $options array
|
||||
* @param $ttl int|array
|
||||
**/
|
||||
function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) {
|
||||
if (!$options)
|
||||
$options=[];
|
||||
$options+=[
|
||||
'group'=>NULL,
|
||||
'order'=>NULL,
|
||||
'limit'=>0,
|
||||
'offset'=>0
|
||||
];
|
||||
$tag='';
|
||||
if (is_array($ttl))
|
||||
list($ttl,$tag)=$ttl;
|
||||
$fw=\Base::instance();
|
||||
$cache=\Cache::instance();
|
||||
if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn().
|
||||
$fw->stringify([$fields,$filter,$options])).($tag?'.'.$tag:'').'.mongo',
|
||||
$result)) || !$ttl || $cached[0]+$ttl<microtime(TRUE)) {
|
||||
if ($options['group']) {
|
||||
$grp=$this->collection->group(
|
||||
$options['group']['keys'],
|
||||
$options['group']['initial'],
|
||||
$options['group']['reduce'],
|
||||
[
|
||||
'condition'=>$filter,
|
||||
'finalize'=>$options['group']['finalize']
|
||||
]
|
||||
);
|
||||
$tmp=$this->db->selectcollection(
|
||||
$fw->HOST.'.'.$fw->BASE.'.'.
|
||||
uniqid(NULL,TRUE).'.tmp'
|
||||
);
|
||||
$tmp->batchinsert($grp['retval'],['w'=>1]);
|
||||
$filter=[];
|
||||
$collection=$tmp;
|
||||
}
|
||||
else {
|
||||
$filter=$filter?:[];
|
||||
$collection=$this->collection;
|
||||
}
|
||||
if ($this->legacy) {
|
||||
$this->cursor=$collection->find($filter,$fields?:[]);
|
||||
if ($options['order'])
|
||||
$this->cursor=$this->cursor->sort($options['order']);
|
||||
if ($options['limit'])
|
||||
$this->cursor=$this->cursor->limit($options['limit']);
|
||||
if ($options['offset'])
|
||||
$this->cursor=$this->cursor->skip($options['offset']);
|
||||
$result=[];
|
||||
while ($this->cursor->hasnext())
|
||||
$result[]=$this->cursor->getnext();
|
||||
}
|
||||
else {
|
||||
$this->cursor=$collection->find($filter,[
|
||||
'sort'=>$options['order'],
|
||||
'limit'=>$options['limit'],
|
||||
'skip'=>$options['offset']
|
||||
]);
|
||||
$result=$this->cursor->toarray();
|
||||
}
|
||||
if ($options['group'])
|
||||
$tmp->drop();
|
||||
if ($fw->CACHE && $ttl)
|
||||
// Save to cache backend
|
||||
$cache->set($hash,$result,$ttl);
|
||||
}
|
||||
$out=[];
|
||||
foreach ($result as $doc)
|
||||
$out[]=$this->factory($doc);
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return records that match criteria
|
||||
* @return static[]
|
||||
* @param $filter array
|
||||
* @param $options array
|
||||
* @param $ttl int|array
|
||||
**/
|
||||
function find($filter=NULL,array $options=NULL,$ttl=0) {
|
||||
if (!$options)
|
||||
$options=[];
|
||||
$options+=[
|
||||
'group'=>NULL,
|
||||
'order'=>NULL,
|
||||
'limit'=>0,
|
||||
'offset'=>0
|
||||
];
|
||||
return $this->select($this->fields,$filter,$options,$ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$fw=\Base::instance();
|
||||
$cache=\Cache::instance();
|
||||
$tag='';
|
||||
if (is_array($ttl))
|
||||
list($ttl,$tag)=$ttl;
|
||||
if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify(
|
||||
[$filter])).($tag?'.'.$tag:'').'.mongo',$result)) || !$ttl ||
|
||||
$cached[0]+$ttl<microtime(TRUE)) {
|
||||
$result=$this->collection->count($filter?:[]);
|
||||
if ($fw->CACHE && $ttl)
|
||||
// Save to cache backend
|
||||
$cache->set($hash,$result,$ttl);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:[];
|
||||
if ($this->document && isset($this->trigger['load']))
|
||||
\Base::instance()->call($this->trigger['load'],$this);
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new record
|
||||
* @return array
|
||||
**/
|
||||
function insert() {
|
||||
if (isset($this->document['_id']))
|
||||
return $this->update();
|
||||
if (isset($this->trigger['beforeinsert']) &&
|
||||
\Base::instance()->call($this->trigger['beforeinsert'],
|
||||
[$this,['_id'=>$this->document['_id']]])===FALSE)
|
||||
return $this->document;
|
||||
if ($this->legacy) {
|
||||
$this->collection->insert($this->document);
|
||||
$pkey=['_id'=>$this->document['_id']];
|
||||
}
|
||||
else {
|
||||
$result=$this->collection->insertone($this->document);
|
||||
$pkey=['_id'=>$result->getinsertedid()];
|
||||
}
|
||||
if (isset($this->trigger['afterinsert']))
|
||||
\Base::instance()->call($this->trigger['afterinsert'],
|
||||
[$this,$pkey]);
|
||||
$this->load($pkey);
|
||||
return $this->document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current record
|
||||
* @return array
|
||||
**/
|
||||
function update() {
|
||||
$pkey=['_id'=>$this->document['_id']];
|
||||
if (isset($this->trigger['beforeupdate']) &&
|
||||
\Base::instance()->call($this->trigger['beforeupdate'],
|
||||
[$this,$pkey])===FALSE)
|
||||
return $this->document;
|
||||
$upsert=['upsert'=>TRUE];
|
||||
if ($this->legacy)
|
||||
$this->collection->update($pkey,$this->document,$upsert);
|
||||
else
|
||||
$this->collection->replaceone($pkey,$this->document,$upsert);
|
||||
if (isset($this->trigger['afterupdate']))
|
||||
\Base::instance()->call($this->trigger['afterupdate'],
|
||||
[$this,$pkey]);
|
||||
return $this->document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete current record
|
||||
* @return bool
|
||||
* @param $quick bool
|
||||
* @param $filter array
|
||||
**/
|
||||
function erase($filter=NULL,$quick=TRUE) {
|
||||
if ($filter) {
|
||||
if (!$quick) {
|
||||
foreach ($this->find($filter) as $mapper)
|
||||
if (!$mapper->erase())
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
return $this->legacy?
|
||||
$this->collection->remove($filter):
|
||||
$this->collection->deletemany($filter);
|
||||
}
|
||||
$pkey=['_id'=>$this->document['_id']];
|
||||
if (isset($this->trigger['beforeerase']) &&
|
||||
\Base::instance()->call($this->trigger['beforeerase'],
|
||||
[$this,$pkey])===FALSE)
|
||||
return FALSE;
|
||||
$result=$this->legacy?
|
||||
$this->collection->remove(['_id'=>$this->document['_id']]):
|
||||
$this->collection->deleteone(['_id'=>$this->document['_id']]);
|
||||
parent::erase();
|
||||
if (isset($this->trigger['aftererase']))
|
||||
\Base::instance()->call($this->trigger['aftererase'],
|
||||
[$this,$pkey]);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cursor
|
||||
* @return NULL
|
||||
**/
|
||||
function reset() {
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cursor from last query
|
||||
* @return object|NULL
|
||||
**/
|
||||
function cursor() {
|
||||
return $this->cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve external iterator for fields
|
||||
* @return object
|
||||
**/
|
||||
function getiterator() {
|
||||
return new \ArrayIterator($this->cast());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate class
|
||||
* @return void
|
||||
* @param $db object
|
||||
* @param $collection string
|
||||
* @param $fields array
|
||||
**/
|
||||
function __construct(\DB\Mongo $db,$collection,$fields=NULL) {
|
||||
$this->db=$db;
|
||||
$this->legacy=$db->legacy();
|
||||
$this->collection=$db->selectcollection($collection);
|
||||
$this->fields=$fields;
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
}
|
||||
194
lib/db/mongo/session.php
Normal file
194
lib/db/mongo/session.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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\Mongo;
|
||||
|
||||
//! MongoDB-managed session handler
|
||||
class Session extends Mapper {
|
||||
|
||||
protected
|
||||
//! Session ID
|
||||
$sid,
|
||||
//! Anti-CSRF token
|
||||
$_csrf,
|
||||
//! User agent
|
||||
$_agent,
|
||||
//! IP,
|
||||
$_ip,
|
||||
//! Suspect callback
|
||||
$onsuspect;
|
||||
|
||||
/**
|
||||
* Open session
|
||||
* @return TRUE
|
||||
* @param $path string
|
||||
* @param $name string
|
||||
**/
|
||||
function open($path,$name) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close session
|
||||
* @return TRUE
|
||||
**/
|
||||
function close() {
|
||||
$this->reset();
|
||||
$this->sid=NULL;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return session data in serialized format
|
||||
* @return string
|
||||
* @param $id string
|
||||
**/
|
||||
function read($id) {
|
||||
$this->load(['session_id'=>$this->sid=$id]);
|
||||
if ($this->dry())
|
||||
return '';
|
||||
if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) {
|
||||
$fw=\Base::instance();
|
||||
if (!isset($this->onsuspect) ||
|
||||
$fw->call($this->onsuspect,[$this,$id])===FALSE) {
|
||||
// NB: `session_destroy` can't be called at that stage;
|
||||
// `session_start` not completed
|
||||
$this->destroy($id);
|
||||
$this->close();
|
||||
unset($fw->{'COOKIE.'.session_name()});
|
||||
$fw->error(403);
|
||||
}
|
||||
}
|
||||
return $this->get('data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write session data
|
||||
* @return TRUE
|
||||
* @param $id string
|
||||
* @param $data string
|
||||
**/
|
||||
function write($id,$data) {
|
||||
$this->set('session_id',$id);
|
||||
$this->set('data',$data);
|
||||
$this->set('ip',$this->_ip);
|
||||
$this->set('agent',$this->_agent);
|
||||
$this->set('stamp',time());
|
||||
$this->save();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy session
|
||||
* @return TRUE
|
||||
* @param $id string
|
||||
**/
|
||||
function destroy($id) {
|
||||
$this->erase(['session_id'=>$id]);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collector
|
||||
* @return TRUE
|
||||
* @param $max int
|
||||
**/
|
||||
function cleanup($max) {
|
||||
$this->erase(['$where'=>'this.stamp+'.$max.'<'.time()]);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return session id (if session has started)
|
||||
* @return string|NULL
|
||||
**/
|
||||
function sid() {
|
||||
return $this->sid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return anti-CSRF token
|
||||
* @return string
|
||||
**/
|
||||
function csrf() {
|
||||
return $this->_csrf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return IP address
|
||||
* @return string
|
||||
**/
|
||||
function ip() {
|
||||
return $this->_ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Unix timestamp
|
||||
* @return string|FALSE
|
||||
**/
|
||||
function stamp() {
|
||||
if (!$this->sid)
|
||||
session_start();
|
||||
return $this->dry()?FALSE:$this->get('stamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return HTTP user agent
|
||||
* @return string
|
||||
**/
|
||||
function agent() {
|
||||
return $this->_agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate class
|
||||
* @param $db \DB\Mongo
|
||||
* @param $table string
|
||||
* @param $onsuspect callback
|
||||
* @param $key string
|
||||
**/
|
||||
function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) {
|
||||
parent::__construct($db,$table);
|
||||
$this->onsuspect=$onsuspect;
|
||||
session_set_save_handler(
|
||||
[$this,'open'],
|
||||
[$this,'close'],
|
||||
[$this,'read'],
|
||||
[$this,'write'],
|
||||
[$this,'destroy'],
|
||||
[$this,'cleanup']
|
||||
);
|
||||
register_shutdown_function('session_commit');
|
||||
$fw=\Base::instance();
|
||||
$headers=$fw->HEADERS;
|
||||
$this->_csrf=$fw->hash($fw->SEED.
|
||||
extension_loaded('openssl')?
|
||||
implode(unpack('L',openssl_random_pseudo_bytes(4))):
|
||||
mt_rand()
|
||||
);
|
||||
if ($key)
|
||||
$fw->$key=$this->_csrf;
|
||||
$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';
|
||||
$this->_ip=$fw->IP;
|
||||
}
|
||||
|
||||
}
|
||||
523
lib/db/sql.php
Normal file
523
lib/db/sql.php
Normal file
@@ -0,0 +1,523 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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;
|
||||
|
||||
//! PDO wrapper
|
||||
class SQL {
|
||||
|
||||
//@{ Error messages
|
||||
const
|
||||
E_PKey='Table %s does not have a primary key';
|
||||
//@}
|
||||
|
||||
const
|
||||
PARAM_FLOAT='float';
|
||||
|
||||
protected
|
||||
//! UUID
|
||||
$uuid,
|
||||
//! Raw PDO
|
||||
$pdo,
|
||||
//! Data source name
|
||||
$dsn,
|
||||
//! Database engine
|
||||
$engine,
|
||||
//! Database name
|
||||
$dbname,
|
||||
//! Transaction flag
|
||||
$trans=FALSE,
|
||||
//! Number of rows affected by query
|
||||
$rows=0,
|
||||
//! SQL log
|
||||
$log;
|
||||
|
||||
/**
|
||||
* Begin SQL transaction
|
||||
* @return bool
|
||||
**/
|
||||
function begin() {
|
||||
$out=$this->pdo->begintransaction();
|
||||
$this->trans=TRUE;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback SQL transaction
|
||||
* @return bool
|
||||
**/
|
||||
function rollback() {
|
||||
$out=$this->pdo->rollback();
|
||||
$this->trans=FALSE;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit SQL transaction
|
||||
* @return bool
|
||||
**/
|
||||
function commit() {
|
||||
$out=$this->pdo->commit();
|
||||
$this->trans=FALSE;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return transaction flag
|
||||
* @return bool
|
||||
**/
|
||||
function trans() {
|
||||
return $this->trans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map data type of argument to a PDO constant
|
||||
* @return int
|
||||
* @param $val scalar
|
||||
**/
|
||||
function type($val) {
|
||||
switch (gettype($val)) {
|
||||
case 'NULL':
|
||||
return \PDO::PARAM_NULL;
|
||||
case 'boolean':
|
||||
return \PDO::PARAM_BOOL;
|
||||
case 'integer':
|
||||
return \PDO::PARAM_INT;
|
||||
case 'resource':
|
||||
return \PDO::PARAM_LOB;
|
||||
case 'float':
|
||||
return self::PARAM_FLOAT;
|
||||
default:
|
||||
return \PDO::PARAM_STR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast value to PHP type
|
||||
* @return mixed
|
||||
* @param $type string
|
||||
* @param $val mixed
|
||||
**/
|
||||
function value($type,$val) {
|
||||
switch ($type) {
|
||||
case self::PARAM_FLOAT:
|
||||
if (!is_string($val))
|
||||
$val=str_replace(',','.',$val);
|
||||
return $val;
|
||||
case \PDO::PARAM_NULL:
|
||||
return NULL;
|
||||
case \PDO::PARAM_INT:
|
||||
return (int)$val;
|
||||
case \PDO::PARAM_BOOL:
|
||||
return (bool)$val;
|
||||
case \PDO::PARAM_STR:
|
||||
return (string)$val;
|
||||
case \PDO::PARAM_LOB:
|
||||
return (binary)$val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute SQL statement(s)
|
||||
* @return array|int|FALSE
|
||||
* @param $cmds string|array
|
||||
* @param $args string|array
|
||||
* @param $ttl int|array
|
||||
* @param $log bool
|
||||
* @param $stamp bool
|
||||
**/
|
||||
function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) {
|
||||
$tag='';
|
||||
if (is_array($ttl))
|
||||
list($ttl,$tag)=$ttl;
|
||||
$auto=FALSE;
|
||||
if (is_null($args))
|
||||
$args=[];
|
||||
elseif (is_scalar($args))
|
||||
$args=[1=>$args];
|
||||
if (is_array($cmds)) {
|
||||
if (count($args)<($count=count($cmds)))
|
||||
// Apply arguments to SQL commands
|
||||
$args=array_fill(0,$count,$args);
|
||||
if (!$this->trans) {
|
||||
$this->begin();
|
||||
$auto=TRUE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$count=1;
|
||||
$cmds=[$cmds];
|
||||
$args=[$args];
|
||||
}
|
||||
if ($this->log===FALSE)
|
||||
$log=FALSE;
|
||||
$fw=\Base::instance();
|
||||
$cache=\Cache::instance();
|
||||
$result=FALSE;
|
||||
for ($i=0;$i<$count;$i++) {
|
||||
$cmd=$cmds[$i];
|
||||
$arg=$args[$i];
|
||||
// ensure 1-based arguments
|
||||
if (array_key_exists(0,$arg)) {
|
||||
array_unshift($arg,'');
|
||||
unset($arg[0]);
|
||||
}
|
||||
if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd))
|
||||
continue;
|
||||
$now=microtime(TRUE);
|
||||
$keys=$vals=[];
|
||||
if ($fw->CACHE && $ttl && ($cached=$cache->exists(
|
||||
$hash=$fw->hash($this->dsn.$cmd.
|
||||
$fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) &&
|
||||
$cached[0]+$ttl>microtime(TRUE)) {
|
||||
foreach ($arg as $key=>$val) {
|
||||
$vals[]=$fw->stringify(is_array($val)?$val[0]:$val);
|
||||
$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key).
|
||||
'/';
|
||||
}
|
||||
if ($log)
|
||||
$this->log.=($stamp?(date('r').' '):'').'('.
|
||||
sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
|
||||
'[CACHED] '.
|
||||
preg_replace($keys,$vals,
|
||||
str_replace('?',chr(0).'?',$cmd),1).PHP_EOL;
|
||||
}
|
||||
elseif (is_object($query=$this->pdo->prepare($cmd))) {
|
||||
foreach ($arg as $key=>$val) {
|
||||
if (is_array($val)) {
|
||||
// User-specified data type
|
||||
$query->bindvalue($key,$val[0],
|
||||
$val[1]==self::PARAM_FLOAT?\PDO::PARAM_STR:$val[1]);
|
||||
$vals[]=$fw->stringify($this->value($val[1],$val[0]));
|
||||
}
|
||||
else {
|
||||
// Convert to PDO data type
|
||||
$query->bindvalue($key,$val,
|
||||
($type=$this->type($val))==self::PARAM_FLOAT?
|
||||
\PDO::PARAM_STR:$type);
|
||||
$vals[]=$fw->stringify($this->value($type,$val));
|
||||
}
|
||||
$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key).
|
||||
'/';
|
||||
}
|
||||
if ($log)
|
||||
$this->log.=($stamp?(date('r').' '):'').'(-0ms) '.
|
||||
preg_replace($keys,$vals,
|
||||
str_replace('?',chr(0).'?',$cmd),1).PHP_EOL;
|
||||
$query->execute();
|
||||
if ($log)
|
||||
$this->log=str_replace('(-0ms)',
|
||||
'('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms)',
|
||||
$this->log);
|
||||
if (($error=$query->errorinfo()) && $error[0]!=\PDO::ERR_NONE) {
|
||||
// Statement-level error occurred
|
||||
if ($this->trans)
|
||||
$this->rollback();
|
||||
user_error('PDOStatement: '.$error[2],E_USER_ERROR);
|
||||
}
|
||||
if (preg_match('/(?:^[\s\(]*'.
|
||||
'(?:WITH|EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) ||
|
||||
(preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) &&
|
||||
$query->columnCount())) {
|
||||
$result=$query->fetchall(\PDO::FETCH_ASSOC);
|
||||
// Work around SQLite quote bug
|
||||
if (preg_match('/sqlite2?/',$this->engine))
|
||||
foreach ($result as $pos=>$rec) {
|
||||
unset($result[$pos]);
|
||||
$result[$pos]=[];
|
||||
foreach ($rec as $key=>$val)
|
||||
$result[$pos][trim($key,'\'"[]`')]=$val;
|
||||
}
|
||||
$this->rows=count($result);
|
||||
if ($fw->CACHE && $ttl)
|
||||
// Save to cache backend
|
||||
$cache->set($hash,$result,$ttl);
|
||||
}
|
||||
else
|
||||
$this->rows=$result=$query->rowcount();
|
||||
$query->closecursor();
|
||||
unset($query);
|
||||
}
|
||||
elseif (($error=$this->errorinfo()) && $error[0]!=\PDO::ERR_NONE) {
|
||||
// PDO-level error occurred
|
||||
if ($this->trans)
|
||||
$this->rollback();
|
||||
user_error('PDO: '.$error[2],E_USER_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
if ($this->trans && $auto)
|
||||
$this->commit();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of rows affected by last query
|
||||
* @return int
|
||||
**/
|
||||
function count() {
|
||||
return $this->rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return SQL profiler results (or disable logging)
|
||||
* @return string
|
||||
* @param $flag bool
|
||||
**/
|
||||
function log($flag=TRUE) {
|
||||
if ($flag)
|
||||
return $this->log;
|
||||
$this->log=FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if table exists
|
||||
* @return bool
|
||||
* @param $table string
|
||||
**/
|
||||
function exists($table) {
|
||||
$mode=$this->pdo->getAttribute(\PDO::ATTR_ERRMODE);
|
||||
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_SILENT);
|
||||
$out=$this->pdo->
|
||||
query('SELECT 1 FROM '.$this->quotekey($table).' LIMIT 1');
|
||||
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE,$mode);
|
||||
return is_object($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve schema of SQL table
|
||||
* @return array|FALSE
|
||||
* @param $table string
|
||||
* @param $fields array|string
|
||||
* @param $ttl int|array
|
||||
**/
|
||||
function schema($table,$fields=NULL,$ttl=0) {
|
||||
$fw=\Base::instance();
|
||||
$cache=\Cache::instance();
|
||||
if ($fw->CACHE && $ttl &&
|
||||
($cached=$cache->exists(
|
||||
$hash=$fw->hash($this->dsn.$table).'.schema',$result)) &&
|
||||
$cached[0]+$ttl>microtime(TRUE))
|
||||
return $result;
|
||||
if (strpos($table,'.'))
|
||||
list($schema,$table)=explode('.',$table);
|
||||
// Supported engines
|
||||
$cmd=[
|
||||
'sqlite2?'=>[
|
||||
'PRAGMA table_info(`'.$table.'`)',
|
||||
'name','type','dflt_value','notnull',0,'pk',TRUE],
|
||||
'mysql'=>[
|
||||
'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`',
|
||||
'Field','Type','Default','Null','YES','Key','PRI'],
|
||||
'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[
|
||||
'SELECT '.
|
||||
'C.COLUMN_NAME AS field,'.
|
||||
'C.DATA_TYPE AS type,'.
|
||||
'C.COLUMN_DEFAULT AS defval,'.
|
||||
'C.IS_NULLABLE AS nullable,'.
|
||||
'T.CONSTRAINT_TYPE AS pkey '.
|
||||
'FROM INFORMATION_SCHEMA.COLUMNS AS C '.
|
||||
'LEFT OUTER JOIN '.
|
||||
'INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K '.
|
||||
'ON '.
|
||||
'C.TABLE_NAME=K.TABLE_NAME AND '.
|
||||
'C.COLUMN_NAME=K.COLUMN_NAME AND '.
|
||||
'C.TABLE_SCHEMA=K.TABLE_SCHEMA '.
|
||||
($this->dbname?
|
||||
('AND C.TABLE_CATALOG=K.TABLE_CATALOG '):'').
|
||||
'LEFT OUTER JOIN '.
|
||||
'INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS T ON '.
|
||||
'K.TABLE_NAME=T.TABLE_NAME AND '.
|
||||
'K.CONSTRAINT_NAME=T.CONSTRAINT_NAME AND '.
|
||||
'K.TABLE_SCHEMA=T.TABLE_SCHEMA '.
|
||||
($this->dbname?
|
||||
('AND K.TABLE_CATALOG=T.TABLE_CATALOG '):'').
|
||||
'WHERE '.
|
||||
'C.TABLE_NAME='.$this->quote($table).
|
||||
($this->dbname?
|
||||
(' AND C.TABLE_CATALOG='.
|
||||
$this->quote($this->dbname)):''),
|
||||
'field','type','defval','nullable','YES','pkey','PRIMARY KEY'],
|
||||
'oci'=>[
|
||||
'SELECT c.column_name AS field, '.
|
||||
'c.data_type AS type, '.
|
||||
'c.data_default AS defval, '.
|
||||
'c.nullable AS nullable, '.
|
||||
'(SELECT t.constraint_type '.
|
||||
'FROM all_cons_columns acc '.
|
||||
'LEFT OUTER JOIN all_constraints t '.
|
||||
'ON acc.constraint_name=t.constraint_name '.
|
||||
'WHERE acc.table_name='.$this->quote($table).' '.
|
||||
'AND acc.column_name=c.column_name '.
|
||||
'AND constraint_type='.$this->quote('P').') AS pkey '.
|
||||
'FROM all_tab_cols c '.
|
||||
'WHERE c.table_name='.$this->quote($table),
|
||||
'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P']
|
||||
];
|
||||
if (is_string($fields))
|
||||
$fields=\Base::instance()->split($fields);
|
||||
$conv=[
|
||||
'int\b|integer'=>\PDO::PARAM_INT,
|
||||
'bool'=>\PDO::PARAM_BOOL,
|
||||
'blob|bytea|image|binary'=>\PDO::PARAM_LOB,
|
||||
'float|real|double|decimal|numeric'=>self::PARAM_FLOAT,
|
||||
'.+'=>\PDO::PARAM_STR
|
||||
];
|
||||
foreach ($cmd as $key=>$val)
|
||||
if (preg_match('/'.$key.'/',$this->engine)) {
|
||||
$rows=[];
|
||||
foreach ($this->exec($val[0],NULL) as $row)
|
||||
if (!$fields || in_array($row[$val[1]],$fields)) {
|
||||
foreach ($conv as $regex=>$type)
|
||||
if (preg_match('/'.$regex.'/i',$row[$val[2]]))
|
||||
break;
|
||||
$rows[$row[$val[1]]]=[
|
||||
'type'=>$row[$val[2]],
|
||||
'pdo_type'=>$type,
|
||||
'default'=>is_string($row[$val[3]])?
|
||||
preg_replace('/^\s*([\'"])(.*)\1\s*/','\2',
|
||||
$row[$val[3]]):$row[$val[3]],
|
||||
'nullable'=>$row[$val[4]]==$val[5],
|
||||
'pkey'=>$row[$val[6]]==$val[7]
|
||||
];
|
||||
}
|
||||
if ($fw->CACHE && $ttl)
|
||||
// Save to cache backend
|
||||
$cache->set($hash,$rows,$ttl);
|
||||
return $rows;
|
||||
}
|
||||
user_error(sprintf(self::E_PKey,$table),E_USER_ERROR);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote string
|
||||
* @return string
|
||||
* @param $val mixed
|
||||
* @param $type int
|
||||
**/
|
||||
function quote($val,$type=\PDO::PARAM_STR) {
|
||||
return $this->engine=='odbc'?
|
||||
(is_string($val)?
|
||||
\Base::instance()->stringify(str_replace('\'','\'\'',$val)):
|
||||
$val):
|
||||
$this->pdo->quote($val,$type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return UUID
|
||||
* @return string
|
||||
**/
|
||||
function uuid() {
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return parent object
|
||||
* @return \PDO
|
||||
**/
|
||||
function pdo() {
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database engine
|
||||
* @return string
|
||||
**/
|
||||
function driver() {
|
||||
return $this->engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return server version
|
||||
* @return string
|
||||
**/
|
||||
function version() {
|
||||
return $this->pdo->getattribute(\PDO::ATTR_SERVER_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database name
|
||||
* @return string
|
||||
**/
|
||||
function name() {
|
||||
return $this->dbname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return quoted identifier name
|
||||
* @return string
|
||||
* @param $key
|
||||
* @param bool $split
|
||||
**/
|
||||
function quotekey($key, $split=TRUE) {
|
||||
$delims=[
|
||||
'sqlite2?|mysql'=>'``',
|
||||
'pgsql|oci'=>'""',
|
||||
'mssql|sqlsrv|odbc|sybase|dblib'=>'[]'
|
||||
];
|
||||
$use='';
|
||||
foreach ($delims as $engine=>$delim)
|
||||
if (preg_match('/'.$engine.'/',$this->engine)) {
|
||||
$use=$delim;
|
||||
break;
|
||||
}
|
||||
return $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key))
|
||||
: $key).$use[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect call to PDO object
|
||||
* @return mixed
|
||||
* @param $func string
|
||||
* @param $args array
|
||||
**/
|
||||
function __call($func,array $args) {
|
||||
return call_user_func_array([$this->pdo,$func],$args);
|
||||
}
|
||||
|
||||
//! Prohibit cloning
|
||||
private function __clone() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate class
|
||||
* @param $dsn string
|
||||
* @param $user string
|
||||
* @param $pw string
|
||||
* @param $options array
|
||||
**/
|
||||
function __construct($dsn,$user=NULL,$pw=NULL,array $options=NULL) {
|
||||
$fw=\Base::instance();
|
||||
$this->uuid=$fw->hash($this->dsn=$dsn);
|
||||
if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts))
|
||||
$this->dbname=$parts[1];
|
||||
if (!$options)
|
||||
$options=[];
|
||||
if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql')
|
||||
$options+=[\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '.
|
||||
strtolower(str_replace('-','',$fw->ENCODING)).';'];
|
||||
$this->pdo=new \PDO($dsn,$user,$pw,$options);
|
||||
$this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME);
|
||||
}
|
||||
|
||||
}
|
||||
759
lib/db/sql/mapper.php
Normal file
759
lib/db/sql/mapper.php
Normal file
@@ -0,0 +1,759 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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\SQL;
|
||||
|
||||
//! SQL data mapper
|
||||
class Mapper extends \DB\Cursor {
|
||||
|
||||
//@{ Error messages
|
||||
const
|
||||
E_PKey='Table %s does not have a primary key';
|
||||
//@}
|
||||
|
||||
protected
|
||||
//! PDO wrapper
|
||||
$db,
|
||||
//! Database engine
|
||||
$engine,
|
||||
//! SQL table
|
||||
$source,
|
||||
//! SQL table (quoted)
|
||||
$table,
|
||||
//! Alias for SQL table
|
||||
$as,
|
||||
//! Last insert ID
|
||||
$_id,
|
||||
//! Defined fields
|
||||
$fields,
|
||||
//! Adhoc fields
|
||||
$adhoc=[],
|
||||
//! Dynamic properties
|
||||
$props=[];
|
||||
|
||||
/**
|
||||
* Return database type
|
||||
* @return string
|
||||
**/
|
||||
function dbtype() {
|
||||
return 'SQL';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return mapped table
|
||||
* @return string
|
||||
**/
|
||||
function table() {
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if any/specified field value has changed
|
||||
* @return bool
|
||||
* @param $key string
|
||||
**/
|
||||
function changed($key=NULL) {
|
||||
if (isset($key))
|
||||
return $this->fields[$key]['changed'];
|
||||
foreach($this->fields as $key=>$field)
|
||||
if ($field['changed'])
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if field is defined
|
||||
* @return bool
|
||||
* @param $key string
|
||||
**/
|
||||
function exists($key) {
|
||||
return array_key_exists($key,$this->fields+$this->adhoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign value to field
|
||||
* @return scalar
|
||||
* @param $key string
|
||||
* @param $val scalar
|
||||
**/
|
||||
function set($key,$val) {
|
||||
if (array_key_exists($key,$this->fields)) {
|
||||
$val=is_null($val) && $this->fields[$key]['nullable']?
|
||||
NULL:$this->db->value($this->fields[$key]['pdo_type'],$val);
|
||||
if ($this->fields[$key]['initial']!==$val ||
|
||||
$this->fields[$key]['default']!==$val && is_null($val))
|
||||
$this->fields[$key]['changed']=TRUE;
|
||||
return $this->fields[$key]['value']=$val;
|
||||
}
|
||||
// Adjust result on existing expressions
|
||||
if (isset($this->adhoc[$key]))
|
||||
$this->adhoc[$key]['value']=$val;
|
||||
elseif (is_string($val))
|
||||
// Parenthesize expression in case it's a subquery
|
||||
$this->adhoc[$key]=['expr'=>'('.$val.')','value'=>NULL];
|
||||
else
|
||||
$this->props[$key]=$val;
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve value of field
|
||||
* @return scalar
|
||||
* @param $key string
|
||||
**/
|
||||
function &get($key) {
|
||||
if ($key=='_id')
|
||||
return $this->_id;
|
||||
elseif (array_key_exists($key,$this->fields))
|
||||
return $this->fields[$key]['value'];
|
||||
elseif (array_key_exists($key,$this->adhoc))
|
||||
return $this->adhoc[$key]['value'];
|
||||
elseif (array_key_exists($key,$this->props))
|
||||
return $this->props[$key];
|
||||
user_error(sprintf(self::E_Field,$key),E_USER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear value of field
|
||||
* @return NULL
|
||||
* @param $key string
|
||||
**/
|
||||
function clear($key) {
|
||||
if (array_key_exists($key,$this->adhoc))
|
||||
unset($this->adhoc[$key]);
|
||||
else
|
||||
unset($this->props[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke dynamic method
|
||||
* @return mixed
|
||||
* @param $func string
|
||||
* @param $args array
|
||||
**/
|
||||
function __call($func,$args) {
|
||||
return call_user_func_array(
|
||||
(array_key_exists($func,$this->props)?
|
||||
$this->props[$func]:
|
||||
$this->$func),$args
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array to mapper object
|
||||
* @return static
|
||||
* @param $row array
|
||||
**/
|
||||
function factory($row) {
|
||||
$mapper=clone($this);
|
||||
$mapper->reset();
|
||||
foreach ($row as $key=>$val) {
|
||||
if (array_key_exists($key,$this->fields))
|
||||
$var='fields';
|
||||
elseif (array_key_exists($key,$this->adhoc))
|
||||
$var='adhoc';
|
||||
else
|
||||
continue;
|
||||
$mapper->{$var}[$key]['value']=$val;
|
||||
$mapper->{$var}[$key]['initial']=$val;
|
||||
if ($var=='fields' && $mapper->{$var}[$key]['pkey'])
|
||||
$mapper->{$var}[$key]['previous']=$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 array_map(
|
||||
function($row) {
|
||||
return $row['value'];
|
||||
},
|
||||
$obj->fields+$obj->adhoc
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query string and arguments
|
||||
* @return array
|
||||
* @param $fields string
|
||||
* @param $filter string|array
|
||||
* @param $options array
|
||||
**/
|
||||
function stringify($fields,$filter=NULL,array $options=NULL) {
|
||||
if (!$options)
|
||||
$options=[];
|
||||
$options+=[
|
||||
'group'=>NULL,
|
||||
'order'=>NULL,
|
||||
'limit'=>0,
|
||||
'offset'=>0,
|
||||
'comment'=>NULL
|
||||
];
|
||||
$db=$this->db;
|
||||
$sql='SELECT '.$fields.' FROM '.$this->table;
|
||||
if (isset($this->as))
|
||||
$sql.=' AS '.$this->db->quotekey($this->as);
|
||||
$args=[];
|
||||
if (is_array($filter)) {
|
||||
$args=isset($filter[1]) && is_array($filter[1])?
|
||||
$filter[1]:
|
||||
array_slice($filter,1,NULL,TRUE);
|
||||
$args=is_array($args)?$args:[1=>$args];
|
||||
list($filter)=$filter;
|
||||
}
|
||||
if ($filter)
|
||||
$sql.=' WHERE '.$filter;
|
||||
if ($options['group']) {
|
||||
$sql.=' GROUP BY '.implode(',',array_map(
|
||||
function($str) use($db) {
|
||||
return preg_replace_callback(
|
||||
'/\b(\w+[._\-\w]*)\h*(HAVING.+|$)/i',
|
||||
function($parts) use($db) {
|
||||
return $db->quotekey($parts[1]).
|
||||
(isset($parts[2])?(' '.$parts[2]):'');
|
||||
},
|
||||
$str
|
||||
);
|
||||
},
|
||||
explode(',',$options['group'])));
|
||||
}
|
||||
if ($options['order']) {
|
||||
$char=substr($db->quotekey(''),0,1);// quoting char
|
||||
$order=' ORDER BY '.(is_bool(strpos($options['order'],$char))?
|
||||
implode(',',array_map(function($str) use($db) {
|
||||
return preg_match('/^\h*(\w+[._\-\w]*)'.
|
||||
'(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i',
|
||||
$str,$parts)?
|
||||
($db->quotekey($parts[1]).
|
||||
(isset($parts[2])?(' '.$parts[2]):'')):$str;
|
||||
},explode(',',$options['order']))):
|
||||
$options['order']);
|
||||
}
|
||||
// SQL Server fixes
|
||||
if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) &&
|
||||
($options['limit'] || $options['offset'])) {
|
||||
// order by pkey when no ordering option was given
|
||||
if (!$options['order'])
|
||||
foreach ($this->fields as $key=>$field)
|
||||
if ($field['pkey']) {
|
||||
$order=' ORDER BY '.$db->quotekey($key);
|
||||
break;
|
||||
}
|
||||
$ofs=$options['offset']?(int)$options['offset']:0;
|
||||
$lmt=$options['limit']?(int)$options['limit']:0;
|
||||
if (strncmp($db->version(),'11',2)>=0) {
|
||||
// SQL Server >= 2012
|
||||
$sql.=$order.' OFFSET '.$ofs.' ROWS';
|
||||
if ($lmt)
|
||||
$sql.=' FETCH NEXT '.$lmt.' ROWS ONLY';
|
||||
}
|
||||
else {
|
||||
// SQL Server 2008
|
||||
$sql=preg_replace('/SELECT/',
|
||||
'SELECT '.
|
||||
($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '.
|
||||
'OVER ('.$order.') AS rnum,',$sql.$order,1);
|
||||
$sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isset($order))
|
||||
$sql.=$order;
|
||||
if ($options['limit'])
|
||||
$sql.=' LIMIT '.(int)$options['limit'];
|
||||
if ($options['offset'])
|
||||
$sql.=' OFFSET '.(int)$options['offset'];
|
||||
}
|
||||
if ($options['comment'])
|
||||
$sql.="\n".' /* '.$options['comment'].' */';
|
||||
return [$sql,$args];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query string and execute
|
||||
* @return static[]
|
||||
* @param $fields string
|
||||
* @param $filter string|array
|
||||
* @param $options array
|
||||
* @param $ttl int|array
|
||||
**/
|
||||
function select($fields,$filter=NULL,array $options=NULL,$ttl=0) {
|
||||
list($sql,$args)=$this->stringify($fields,$filter,$options);
|
||||
$result=$this->db->exec($sql,$args,$ttl);
|
||||
$out=[];
|
||||
foreach ($result as &$row) {
|
||||
foreach ($row as $field=>&$val) {
|
||||
if (array_key_exists($field,$this->fields)) {
|
||||
if (!is_null($val) || !$this->fields[$field]['nullable'])
|
||||
$val=$this->db->value(
|
||||
$this->fields[$field]['pdo_type'],$val);
|
||||
}
|
||||
unset($val);
|
||||
}
|
||||
$out[]=$this->factory($row);
|
||||
unset($row);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return records that match criteria
|
||||
* @return static[]
|
||||
* @param $filter string|array
|
||||
* @param $options array
|
||||
* @param $ttl int|array
|
||||
**/
|
||||
function find($filter=NULL,array $options=NULL,$ttl=0) {
|
||||
if (!$options)
|
||||
$options=[];
|
||||
$options+=[
|
||||
'group'=>NULL,
|
||||
'order'=>NULL,
|
||||
'limit'=>0,
|
||||
'offset'=>0
|
||||
];
|
||||
$adhoc='';
|
||||
foreach ($this->adhoc as $key=>$field)
|
||||
$adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key);
|
||||
return $this->select(
|
||||
($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)?
|
||||
$options['group']:
|
||||
implode(',',array_map([$this->db,'quotekey'],
|
||||
array_keys($this->fields)))).$adhoc,$filter,$options,$ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count records that match criteria
|
||||
* @return int
|
||||
* @param $filter string|array
|
||||
* @param $options array
|
||||
* @param $ttl int|array
|
||||
**/
|
||||
function count($filter=NULL,array $options=NULL,$ttl=0) {
|
||||
$adhoc=[];
|
||||
// with grouping involved, we need to wrap the actualy query and count the results
|
||||
if ($subquery_mode=($options && !empty($options['group']))) {
|
||||
$group_string=preg_replace('/HAVING.+$/i','',$options['group']);
|
||||
$group_fields=array_flip(array_map('trim',explode(',',$group_string)));
|
||||
foreach ($this->adhoc as $key=>$field)
|
||||
// add adhoc fields that are used for grouping
|
||||
if (isset($group_fields[$key]))
|
||||
$adhoc[]=$field['expr'].' AS '.$this->db->quotekey($key);
|
||||
$fields=implode(',',$adhoc);
|
||||
if (empty($fields))
|
||||
// Select at least one field, ideally the grouping fields
|
||||
// or sqlsrv fails
|
||||
$fields=$group_string;
|
||||
if (preg_match('/mssql|dblib|sqlsrv/',$this->engine))
|
||||
$fields='TOP 100 PERCENT '.$fields;
|
||||
} else {
|
||||
// for simple count just add a new adhoc counter
|
||||
$fields='COUNT(*) AS '.$this->db->quotekey('_rows');
|
||||
}
|
||||
list($sql,$args)=$this->stringify($fields,$filter,$options);
|
||||
if ($subquery_mode)
|
||||
$sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '.
|
||||
'FROM ('.$sql.') AS '.$this->db->quotekey('_temp');
|
||||
$result=$this->db->exec($sql,$args,$ttl);
|
||||
unset($this->adhoc['_rows']);
|
||||
return (int)$result[0]['_rows'];
|
||||
}
|
||||
/**
|
||||
* Return record at specified offset using same criteria as
|
||||
* previous load() call and make it active
|
||||
* @return static
|
||||
* @param $ofs int
|
||||
**/
|
||||
function skip($ofs=1) {
|
||||
$out=parent::skip($ofs);
|
||||
$dry=$this->dry();
|
||||
foreach ($this->fields as $key=>&$field) {
|
||||
$field['value']=$dry?NULL:$out->fields[$key]['value'];
|
||||
$field['initial']=$field['value'];
|
||||
$field['changed']=FALSE;
|
||||
if ($field['pkey'])
|
||||
$field['previous']=$dry?NULL:$out->fields[$key]['value'];
|
||||
unset($field);
|
||||
}
|
||||
foreach ($this->adhoc as $key=>&$field) {
|
||||
$field['value']=$dry?NULL:$out->adhoc[$key]['value'];
|
||||
unset($field);
|
||||
}
|
||||
if (!$dry && isset($this->trigger['load']))
|
||||
\Base::instance()->call($this->trigger['load'],$this);
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new record
|
||||
* @return static
|
||||
**/
|
||||
function insert() {
|
||||
$args=[];
|
||||
$actr=0;
|
||||
$nctr=0;
|
||||
$fields='';
|
||||
$values='';
|
||||
$filter='';
|
||||
$pkeys=[];
|
||||
$nkeys=[];
|
||||
$ckeys=[];
|
||||
$inc=NULL;
|
||||
foreach ($this->fields as $key=>$field)
|
||||
if ($field['pkey'])
|
||||
$pkeys[$key]=$field['previous'];
|
||||
if (isset($this->trigger['beforeinsert']) &&
|
||||
\Base::instance()->call($this->trigger['beforeinsert'],
|
||||
[$this,$pkeys])===FALSE)
|
||||
return $this;
|
||||
if ($this->valid())
|
||||
// duplicate record
|
||||
foreach ($this->fields as $key=>&$field) {
|
||||
$field['changed']=true;
|
||||
if ($field['pkey'] && !$inc && $field['pdo_type']==\PDO::PARAM_INT
|
||||
&& !$field['nullable'])
|
||||
$inc=$key;
|
||||
unset($field);
|
||||
}
|
||||
foreach ($this->fields as $key=>&$field) {
|
||||
if ($field['pkey']) {
|
||||
$field['previous']=$field['value'];
|
||||
if (!$inc && $field['pdo_type']==\PDO::PARAM_INT &&
|
||||
empty($field['value']) && !$field['nullable'] &&
|
||||
is_null($field['default']))
|
||||
$inc=$key;
|
||||
$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
|
||||
$nkeys[$nctr+1]=[$field['value'],$field['pdo_type']];
|
||||
$nctr++;
|
||||
}
|
||||
if ($field['changed'] && $key!=$inc) {
|
||||
$fields.=($actr?',':'').$this->db->quotekey($key);
|
||||
$values.=($actr?',':'').'?';
|
||||
$args[$actr+1]=[$field['value'],$field['pdo_type']];
|
||||
$actr++;
|
||||
$ckeys[]=$key;
|
||||
}
|
||||
unset($field);
|
||||
}
|
||||
if ($fields) {
|
||||
$add=$aik='';
|
||||
if ($this->engine=='pgsql' && !empty($pkeys)) {
|
||||
$names=array_keys($pkeys);
|
||||
$aik=end($names);
|
||||
$add=' RETURNING '.$this->db->quotekey($aik);
|
||||
}
|
||||
$lID=$this->db->exec(
|
||||
(preg_match('/mssql|dblib|sqlsrv/',$this->engine) &&
|
||||
array_intersect(array_keys($pkeys),$ckeys)?
|
||||
'SET IDENTITY_INSERT '.$this->table.' ON;':'').
|
||||
'INSERT INTO '.$this->table.' ('.$fields.') '.
|
||||
'VALUES ('.$values.')'.$add,$args
|
||||
);
|
||||
if ($this->engine=='pgsql' && $lID && $aik)
|
||||
$this->_id=$lID[0][$aik];
|
||||
elseif ($this->engine!='oci')
|
||||
$this->_id=$this->db->lastinsertid();
|
||||
// Reload to obtain default and auto-increment field values
|
||||
if ($reload=(($inc && $this->_id) || $filter))
|
||||
$this->load($inc?
|
||||
[$inc.'=?',$this->db->value(
|
||||
$this->fields[$inc]['pdo_type'],$this->_id)]:
|
||||
[$filter,$nkeys]);
|
||||
if (isset($this->trigger['afterinsert']))
|
||||
\Base::instance()->call($this->trigger['afterinsert'],
|
||||
[$this,$pkeys]);
|
||||
// reset changed flag after calling afterinsert
|
||||
if (!$reload)
|
||||
foreach ($this->fields as $key=>&$field) {
|
||||
$field['changed']=FALSE;
|
||||
$field['initial']=$field['value'];
|
||||
unset($field);
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current record
|
||||
* @return static
|
||||
**/
|
||||
function update() {
|
||||
$args=[];
|
||||
$ctr=0;
|
||||
$pairs='';
|
||||
$pkeys=[];
|
||||
foreach ($this->fields as $key=>$field)
|
||||
if ($field['pkey'])
|
||||
$pkeys[$key]=$field['previous'];
|
||||
if (isset($this->trigger['beforeupdate']) &&
|
||||
\Base::instance()->call($this->trigger['beforeupdate'],
|
||||
[$this,$pkeys])===FALSE)
|
||||
return $this;
|
||||
foreach ($this->fields as $key=>$field)
|
||||
if ($field['changed']) {
|
||||
$pairs.=($pairs?',':'').$this->db->quotekey($key).'=?';
|
||||
$args[++$ctr]=[$field['value'],$field['pdo_type']];
|
||||
}
|
||||
if ($pairs) {
|
||||
$filter='';
|
||||
foreach ($this->fields as $key=>$field)
|
||||
if ($field['pkey']) {
|
||||
$filter.=($filter?' AND ':' WHERE ').
|
||||
$this->db->quotekey($key).'=?';
|
||||
$args[++$ctr]=[$field['previous'],$field['pdo_type']];
|
||||
}
|
||||
if (!$filter)
|
||||
user_error(sprintf(self::E_PKey,$this->source),E_USER_ERROR);
|
||||
$sql='UPDATE '.$this->table.' SET '.$pairs.$filter;
|
||||
$this->db->exec($sql,$args);
|
||||
}
|
||||
if (isset($this->trigger['afterupdate']))
|
||||
\Base::instance()->call($this->trigger['afterupdate'],
|
||||
[$this,$pkeys]);
|
||||
// reset changed flag after calling afterupdate
|
||||
foreach ($this->fields as $key=>&$field) {
|
||||
$field['changed']=FALSE;
|
||||
$field['initial']=$field['value'];
|
||||
unset($field);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* batch-update multiple records at once
|
||||
* @param string|array $filter
|
||||
* @return int
|
||||
*/
|
||||
function updateAll($filter=NULL) {
|
||||
$args=[];
|
||||
$ctr=$out=0;
|
||||
$pairs='';
|
||||
foreach ($this->fields as $key=>$field)
|
||||
if ($field['changed']) {
|
||||
$pairs.=($pairs?',':'').$this->db->quotekey($key).'=?';
|
||||
$args[++$ctr]=[$field['value'],$field['pdo_type']];
|
||||
}
|
||||
if ($filter)
|
||||
if (is_array($filter)) {
|
||||
$cond=array_shift($filter);
|
||||
$args=array_merge($args,$filter);
|
||||
$filter=' WHERE '.$cond;
|
||||
} else
|
||||
$filter=' WHERE '.$filter;
|
||||
if ($pairs) {
|
||||
$sql='UPDATE '.$this->table.' SET '.$pairs.$filter;
|
||||
$out = $this->db->exec($sql,$args);
|
||||
}
|
||||
// reset changed flag after calling afterupdate
|
||||
foreach ($this->fields as $key=>&$field) {
|
||||
$field['changed']=FALSE;
|
||||
$field['initial']=$field['value'];
|
||||
unset($field);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete current record
|
||||
* @return int
|
||||
* @param $quick bool
|
||||
* @param $filter string|array
|
||||
**/
|
||||
function erase($filter=NULL,$quick=TRUE) {
|
||||
if (isset($filter)) {
|
||||
if (!$quick) {
|
||||
$out=0;
|
||||
foreach ($this->find($filter) as $mapper)
|
||||
$out+=$mapper->erase();
|
||||
return $out;
|
||||
}
|
||||
$args=[];
|
||||
if (is_array($filter)) {
|
||||
$args=isset($filter[1]) && is_array($filter[1])?
|
||||
$filter[1]:
|
||||
array_slice($filter,1,NULL,TRUE);
|
||||
$args=is_array($args)?$args:[1=>$args];
|
||||
list($filter)=$filter;
|
||||
}
|
||||
return $this->db->
|
||||
exec('DELETE FROM '.$this->table.
|
||||
($filter?' WHERE '.$filter:'').';',$args);
|
||||
}
|
||||
$args=[];
|
||||
$ctr=0;
|
||||
$filter='';
|
||||
$pkeys=[];
|
||||
foreach ($this->fields as $key=>&$field) {
|
||||
if ($field['pkey']) {
|
||||
$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
|
||||
$args[$ctr+1]=[$field['previous'],$field['pdo_type']];
|
||||
$pkeys[$key]=$field['previous'];
|
||||
$ctr++;
|
||||
}
|
||||
$field['value']=NULL;
|
||||
$field['changed']=(bool)$field['default'];
|
||||
if ($field['pkey'])
|
||||
$field['previous']=NULL;
|
||||
unset($field);
|
||||
}
|
||||
if (!$filter)
|
||||
user_error(sprintf(self::E_PKey,$this->source),E_USER_ERROR);
|
||||
foreach ($this->adhoc as &$field) {
|
||||
$field['value']=NULL;
|
||||
unset($field);
|
||||
}
|
||||
parent::erase();
|
||||
if (isset($this->trigger['beforeerase']) &&
|
||||
\Base::instance()->call($this->trigger['beforeerase'],
|
||||
[$this,$pkeys])===FALSE)
|
||||
return 0;
|
||||
$out=$this->db->
|
||||
exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args);
|
||||
if (isset($this->trigger['aftererase']))
|
||||
\Base::instance()->call($this->trigger['aftererase'],
|
||||
[$this,$pkeys]);
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cursor
|
||||
* @return NULL
|
||||
**/
|
||||
function reset() {
|
||||
foreach ($this->fields as &$field) {
|
||||
$field['value']=NULL;
|
||||
$field['initial']=NULL;
|
||||
$field['changed']=FALSE;
|
||||
if ($field['pkey'])
|
||||
$field['previous']=NULL;
|
||||
unset($field);
|
||||
}
|
||||
foreach ($this->adhoc as &$field) {
|
||||
$field['value']=NULL;
|
||||
unset($field);
|
||||
}
|
||||
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)
|
||||
if (in_array($key,array_keys($this->fields)))
|
||||
$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->fields+$this->adhoc as $key=>$field)
|
||||
$var[$key]=$field['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return schema and, if the first argument is provided, update it
|
||||
* @return array
|
||||
* @param $fields NULL|array
|
||||
**/
|
||||
function schema($fields=null) {
|
||||
if ($fields)
|
||||
$this->fields = $fields;
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return field names
|
||||
* @return array
|
||||
* @param $adhoc bool
|
||||
**/
|
||||
function fields($adhoc=TRUE) {
|
||||
return array_keys($this->fields+($adhoc?$this->adhoc:[]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if field is not nullable
|
||||
* @return bool
|
||||
* @param $field string
|
||||
**/
|
||||
function required($field) {
|
||||
return isset($this->fields[$field]) &&
|
||||
!$this->fields[$field]['nullable'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve external iterator for fields
|
||||
* @return object
|
||||
**/
|
||||
function getiterator() {
|
||||
return new \ArrayIterator($this->cast());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign alias for table
|
||||
* @param $alias string
|
||||
**/
|
||||
function alias($alias) {
|
||||
$this->as=$alias;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate class
|
||||
* @param $db \DB\SQL
|
||||
* @param $table string
|
||||
* @param $fields array|string
|
||||
* @param $ttl int|array
|
||||
**/
|
||||
function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) {
|
||||
$this->db=$db;
|
||||
$this->engine=$db->driver();
|
||||
if ($this->engine=='oci')
|
||||
$table=strtoupper($table);
|
||||
$this->source=$table;
|
||||
$this->table=$this->db->quotekey($table);
|
||||
$this->fields=$db->schema($table,$fields,$ttl);
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
}
|
||||
222
lib/db/sql/session.php
Normal file
222
lib/db/sql/session.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009-2019 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\SQL;
|
||||
|
||||
//! SQL-managed session handler
|
||||
class Session extends Mapper {
|
||||
|
||||
protected
|
||||
//! Session ID
|
||||
$sid,
|
||||
//! Anti-CSRF token
|
||||
$_csrf,
|
||||
//! User agent
|
||||
$_agent,
|
||||
//! IP,
|
||||
$_ip,
|
||||
//! Suspect callback
|
||||
$onsuspect;
|
||||
|
||||
/**
|
||||
* Open session
|
||||
* @return TRUE
|
||||
* @param $path string
|
||||
* @param $name string
|
||||
**/
|
||||
function open($path,$name) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close session
|
||||
* @return TRUE
|
||||
**/
|
||||
function close() {
|
||||
$this->reset();
|
||||
$this->sid=NULL;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return session data in serialized format
|
||||
* @return string
|
||||
* @param $id string
|
||||
**/
|
||||
function read($id) {
|
||||
$this->load(['session_id=?',$this->sid=$id]);
|
||||
if ($this->dry())
|
||||
return '';
|
||||
if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) {
|
||||
$fw=\Base::instance();
|
||||
if (!isset($this->onsuspect) ||
|
||||
$fw->call($this->onsuspect,[$this,$id])===FALSE) {
|
||||
//NB: `session_destroy` can't be called at that stage (`session_start` not completed)
|
||||
$this->destroy($id);
|
||||
$this->close();
|
||||
unset($fw->{'COOKIE.'.session_name()});
|
||||
$fw->error(403);
|
||||
}
|
||||
}
|
||||
return $this->get('data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write session data
|
||||
* @return TRUE
|
||||
* @param $id string
|
||||
* @param $data string
|
||||
**/
|
||||
function write($id,$data) {
|
||||
$this->set('session_id',$id);
|
||||
$this->set('data',$data);
|
||||
$this->set('ip',$this->_ip);
|
||||
$this->set('agent',$this->_agent);
|
||||
$this->set('stamp',time());
|
||||
$this->save();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy session
|
||||
* @return TRUE
|
||||
* @param $id string
|
||||
**/
|
||||
function destroy($id) {
|
||||
$this->erase(['session_id=?',$id]);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collector
|
||||
* @return TRUE
|
||||
* @param $max int
|
||||
**/
|
||||
function cleanup($max) {
|
||||
$this->erase(['stamp+?<?',$max,time()]);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return session id (if session has started)
|
||||
* @return string|NULL
|
||||
**/
|
||||
function sid() {
|
||||
return $this->sid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return anti-CSRF token
|
||||
* @return string
|
||||
**/
|
||||
function csrf() {
|
||||
return $this->_csrf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return IP address
|
||||
* @return string
|
||||
**/
|
||||
function ip() {
|
||||
return $this->_ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Unix timestamp
|
||||
* @return string|FALSE
|
||||
**/
|
||||
function stamp() {
|
||||
if (!$this->sid)
|
||||
session_start();
|
||||
return $this->dry()?FALSE:$this->get('stamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return HTTP user agent
|
||||
* @return string
|
||||
**/
|
||||
function agent() {
|
||||
return $this->_agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate class
|
||||
* @param $db \DB\SQL
|
||||
* @param $table string
|
||||
* @param $force bool
|
||||
* @param $onsuspect callback
|
||||
* @param $key string
|
||||
* @param $type string, column type for data field
|
||||
**/
|
||||
function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$key=NULL,$type='TEXT') {
|
||||
if ($force) {
|
||||
$eol="\n";
|
||||
$tab="\t";
|
||||
$sqlsrv=preg_match('/mssql|sqlsrv|sybase/',$db->driver());
|
||||
$db->exec(
|
||||
($sqlsrv?
|
||||
('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '.
|
||||
'name='.$db->quote($table).' AND xtype=\'U\') '.
|
||||
'CREATE TABLE dbo.'):
|
||||
('CREATE TABLE IF NOT EXISTS '.
|
||||
((($name=$db->name())&&$db->driver()!='pgsql')?
|
||||
($db->quotekey($name,FALSE).'.'):''))).
|
||||
$db->quotekey($table,FALSE).' ('.$eol.
|
||||
($sqlsrv?$tab.$db->quotekey('id').' INT IDENTITY,'.$eol:'').
|
||||
$tab.$db->quotekey('session_id').' VARCHAR(255),'.$eol.
|
||||
$tab.$db->quotekey('data').' '.$type.','.$eol.
|
||||
$tab.$db->quotekey('ip').' VARCHAR(45),'.$eol.
|
||||
$tab.$db->quotekey('agent').' VARCHAR(300),'.$eol.
|
||||
$tab.$db->quotekey('stamp').' INTEGER,'.$eol.
|
||||
$tab.'PRIMARY KEY ('.$db->quotekey($sqlsrv?'id':'session_id').')'.$eol.
|
||||
($sqlsrv?',CONSTRAINT [UK_session_id] UNIQUE(session_id)':'').
|
||||
');'
|
||||
);
|
||||
}
|
||||
parent::__construct($db,$table);
|
||||
$this->onsuspect=$onsuspect;
|
||||
session_set_save_handler(
|
||||
[$this,'open'],
|
||||
[$this,'close'],
|
||||
[$this,'read'],
|
||||
[$this,'write'],
|
||||
[$this,'destroy'],
|
||||
[$this,'cleanup']
|
||||
);
|
||||
register_shutdown_function('session_commit');
|
||||
$fw=\Base::instance();
|
||||
$headers=$fw->HEADERS;
|
||||
$this->_csrf=$fw->hash($fw->SEED.
|
||||
extension_loaded('openssl')?
|
||||
implode(unpack('L',openssl_random_pseudo_bytes(4))):
|
||||
mt_rand()
|
||||
);
|
||||
if ($key)
|
||||
$fw->$key=$this->_csrf;
|
||||
$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';
|
||||
if (strlen($this->_agent) > 300) {
|
||||
$this->_agent = substr($this->_agent, 0, 300);
|
||||
}
|
||||
$this->_ip=$fw->IP;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user