| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759 |
- <?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();
- }
- }
|