f536e13c113a035c49888852ec497cc345f309ad
[fa-stable.git] / includes / db / class.data_set.inc
1 <?php
2 /**********************************************************************
3     Copyright (C) FrontAccounting, LLC.
4         Released under the terms of the GNU General Public License, GPL, 
5         as published by the Free Software Foundation, either version 3 
6         of the License, or (at your option) any later version.
7     This program is distributed in the hope that it will be useful,
8     but WITHOUT ANY WARRANTY; without even the implied warranty of
9     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
10     See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
11 ***********************************************************************/
12
13 // array contains optional/required params for all supported validator types
14 // 
15 $validator_types = array(
16         'required' => array(),
17         'range' => array('min'=>0, 'max' =>null, 'lt', 'gt', 'le', 'ge')
18 );
19
20 /*
21         Generic record set object
22 */
23 abstract class record_set {
24         var $key;                       // unique key fields
25         var $fields;            // other fields
26         var $name;                      // table name (without prefix)
27         var $checks = array();          // checks on data before insert/update/delete
28
29         var $selected_id;       // current key
30         var $data;                      // current record data
31         var $errors = array();
32         var $debug = array();
33         var $subset;            // optional where clause for record subset
34
35         function __wakeup()
36         {
37                 $this->errors = array();
38         }
39
40         function record_set($name, $fields, $key, $subset=null)
41         {
42                 $this->name = $name;
43                 $this->fields = $fields;
44                 $this->key = $key;
45                 $this->subset = $subset;
46         }
47
48         function error($msg, $field=null) {
49                 global $db;
50
51                 // save error message
52                 if (!isset($field))
53                         $field = count($this->errors);
54                 $this->errors[$field] = $msg;
55
56                 // save db errors for debugging purposes
57                 if ($db && db_error_no()) $this->debug[] = db_error_msg($db);
58
59                 return false;
60         }
61         //==============================================================================================
62         // 
63         // Data valiation
64
65         /**
66         *       Set validator for insert/update/delete
67         *
68         *       @check - validator description string in format: 'field_name:uid_modes:check_type[[:param1][:param2]...]'
69         *   parameters can be expressed with name in form: name=value
70         *       @msg - message stored on error
71         **/
72         
73         function set_validator($check, $msg=null)
74         {
75                 $params = explode(':', $check);
76                 if (count($params)<3)
77                         display_error(_('Invalid validator string'));
78
79                 $fieldname = array_shift($params);
80                 $mode = array_shift($params);
81                 $type = array_shift($params);
82                 $options = array();
83                 if (count($params))
84                 {
85                         foreach ($params as $par) {
86                                 if (($n = strpos($par, '=')) !== false) {
87                                         $options[substr($par, 0, $n)] = substr($par, $n+1);
88                                 } else
89                                         $options[] = $par;
90                         }
91                 }
92                 $this->checks[] = array('fld'=>$fieldname, 'type' => $type, 'msg' => $msg, 'opts' => $options, 'mode'=>$mode);
93         }
94
95         /**
96         *       Validate data
97         *       @key - record key (u/d)
98         *       @data - data to be validated (i/u)
99         **/
100         function _validate($key=null, $data)
101         {
102                 $mode = isset($data) ? (isset($key) ? 'u' : 'i') : 'd';
103
104                 foreach($this->checks as $check) {
105
106                         if (strpos($check['mode'], $mode) !== false) {
107                                 $msg = $check['msg'];
108                                 $fld = $check['fld'];
109                                 $opts = @$check['opts'];
110
111                                 // precheck for existing 
112                                 if ($mode == 'i' && $fld && !isset($data[$fld])) {
113                                         $msg = sprintf(_("Input parameter '%s' have to be set."), $check['fld']);
114                                         return $this->error($msg, $check['fld']);
115                                 }
116
117                                 switch($check['type']) {
118
119                                         case 'required':
120                                                 if ($data[$fld]==='') {
121                                                         if (!$msg) $msg = sprintf(_("Parameter '%s' cannot be empty.", $check['fld']));
122                                                                 return $this->error($msg, $check['fld']);
123                                                 }
124                                                 break;
125
126                                         case 'clean_file_name':
127                                                 if (isset($data[$fld]) && ($data[$fld] != clean_file_name($data[$fld]))) {
128                                                         if (!$msg) $msg = sprintf(_("Parameter '%s' contains invalid characters.", $check['fld']));
129                                                         return $this->error($msg, $check['fld']);
130                                                 }
131                                                 break;
132
133                                         case 'range':
134                                                 if (!$msg) $msg = sprintf(_("Parameter '%s' has invalid value.", $check['fld']));
135                                                 // TODO: check at least one named parameter is passed
136                                                 if (isset($opts['lt']) && !($data[$fld] < $opts['lt']) ||
137                                                         (isset($opts['gt']) && !($data[$fld] > $opts['gt'])) ||
138                                                         (isset($opts['min']) && !($data[$fld] < $opts['min'])) ||
139                                                         (isset($opts['max']) && !($data[$fld] > $opts['max'])) )
140                                                                 return $this->error($msg, $check['fld']);
141
142 //                                      case 'match':
143                                                 break;
144
145                                         // user defined checks
146                                         default:
147                                                 $func = $check['type'];
148                                                 if (method_exists($this, $func)) {
149                                                         if (!$this->$func($data, $check['opts'], $key))
150                                                                 return $this->error($msg, $check['fld']);
151                                                 } else if (function_exists($func)) {
152                                                         if (!$func($data, $key, $check['opts'], $key))
153                                                                 return $this->error($msg, $check['fld']);
154                                                 }
155                                 }
156                         }
157                 }
158                 return true;
159         }
160
161         /**
162         *
163         *       Returns editable status for selected record.
164         *       Array contains true for editable, false for readonly fields.
165         *       This looks redundant ('forbidden' update_check could be used)
166         *       but in fact the constraints are related to exact record, so changes with key.
167         **/
168         function edit_status($key)
169         {
170                 $editables = array();
171
172                 // default: all but key fields editable 
173                 foreach ($this->fields as $fld=>$val)
174                         $editables[$fld] = !in_array($fld, (array)$this->key);
175
176                 return $editables;
177         }
178
179
180         function delete_check($key)
181         {
182                 return $this->_validate($key, null);
183         }
184
185         function insert_check($data)
186         {
187                 return $this->_validate(null, $data);
188         }
189
190         function update_check($key, $data)
191         {
192                 // Note: this does not allow change of key
193                 return $this->_validate( $key, $data);
194         }
195
196         //===========================================================================
197         //      Database functions placeholders
198         
199         //
200         //      Generic read record routine
201         //
202         function get($key=null)
203         {
204                 $defaults = array();
205                 // return all defined default values
206                 foreach ($this->fields as $name => $def)
207                 {
208                         if(!is_numeric($name)) {
209                                 if (is_string($def))
210                                         $defaults[$name] = $def;
211                                 elseif (isset($def['dflt']))
212                                         $defaults[$name] = $def['dflt'];
213                         }
214                 }
215                 return $defaults;
216         }
217         //
218         //      Generic list record routine
219         //
220         abstract function get_all();
221         //
222         //      Generic update record routine
223         //
224         function update($key, $data)
225         {
226                 if (!$this->update_check($key, $data))
227                         return false;
228
229                 return true;
230         }
231         //
232         //      Generic delete record routine
233         //
234         function delete($key)
235         {
236                 if (!$this->delete_check($key))
237                         return false;
238
239                 return true;
240         }
241         //
242         //      Insert record
243         //
244         function insert($data)
245         {
246                 if (!$this->insert_check($data))
247                         return false;
248
249                 return true;
250         }
251
252 }
253
254 class data_set extends record_set {
255
256         function data_set($name, $fields, $key, $subset=null)
257         {
258                 $this->record_set($name, $fields, $key, $subset);
259         }
260
261         //
262         //      Generic read record routine
263         //
264         function get($key=null)
265         {
266                 if ($key===null)
267                         return parent::get();
268
269                 $sql = "SELECT * FROM ".TB_PREF.$this->name." WHERE ";
270                 $where = $this->subset ? (array)$this->subset : array();
271
272                 if (is_array($this->key)) {
273                         foreach($this->key as $fld)
274                                 if (isset($key[$fld]))
275                                         $where[$fld] = "`$fld`=".db_escape($key[$fld]);
276                                 else
277                                         return $this->error(sprintf(_("Invalid key passed reading '%s'"), $this->name));
278                 } else {
279                         $where = array($this->key => "`".$this->key."`=".db_escape($key));
280                 }
281
282                 $sql .= implode(' AND ', $where);
283                 $result = db_query($sql);
284                 if (!$result)
285                         return $this->error("Cannot get record from ".$this->name);
286
287                 return $rec = db_num_rows($result) ? db_fetch_assoc($result) : null;
288         }
289         //
290         //      Generic list record routine
291         //
292         function get_all($where=null, $order_by=null)
293         {
294                 $fields = array();
295                 foreach($this->fields as $fld)
296                         $fields[] = '`'.$fld.'`';
297                 $sql = "SELECT ".implode(',', $fields)." FROM ".TB_PREF.$this->name;
298
299                 if ($where)
300                         $sql .= " WHERE ".($this->subset ? '('.$this->subset . ') AND ' : ''). $where;
301                 if ($order_by) {
302                         $order_by = (array)$order_by;
303                         foreach($order_by as $i => $fld)
304                                 $order_by[$i] = '`'.$fld.'`';
305                         $sql .= " ORDER BY ".implode(',', (array)$order_by);
306                 }
307                 $result = db_query($sql);
308                 if ($result==false)
309                         return $this->error("Cannot get record from ".$this->name);
310
311                 return $result;
312         }
313         //
314         //      Generic update record routine
315         //
316         function update($key, $data)
317         {
318                 if (!parent::update($key, $data))       // validate data
319                         return false;
320
321                 $sql = "UPDATE ".TB_PREF.$this->name." SET ";
322                 $updates = array();
323
324                 foreach($data as $fld => $value) {      // select only data relevant for this model
325                         if (in_array($fld, $this->fields))
326                                 $updates[$fld] = "`$fld`=".db_escape($value);
327                 }
328                 if (count($updates) == 0)
329                         return $this->error(_("Empty update data for table ").$this->name);
330
331                 $sql .= implode(',', $updates)." WHERE ";
332                 $where = $this->subset ? (array)$this->subset : array();
333
334                 if(is_array($this->key)) {                      // construct key phrase
335                         foreach($this->key as $fld)
336                                 if (isset($key[$fld]))
337                                         $where[$fld] = "`$fld`=".db_escape($key[$fld]);
338                                 else
339                                         return $this->error(sprintf(_("Invalid key for update '%s'"), $this->name));
340                 } else {
341                         $where = array("`".$this->key."`=".db_escape($key));
342                 }
343
344                 $sql .= implode(' AND ', $where);
345                 $result = db_query($sql);
346
347                 if ($result===false)
348                         return $this->error("cannot update record in ".$this->name);
349
350                 return $result;
351         }
352         //
353         //      Generic delete record routine
354         //
355         function delete($key)
356         {
357                 if (!parent::delete_check($key))
358                         return false;
359
360                 $sql = "DELETE FROM ".TB_PREF.$this->name;
361                 $where = $this->subset ? (array)$this->subset : array();
362
363                 if(is_array($this->key)) {
364                         foreach($this->key as $fld)
365                                 if (isset($key[$fld]))
366                                         $where[$fld] = "`$fld`=".db_escape($key[$fld]);
367                                 else
368                                         return $this->error(sprintf(_("Invalid key for update '%s'"), $this->name));
369                 } else {
370                         $where = array("`".$this->key."`=".db_escape($key));
371                 }
372
373                 $sql .= " WHERE ".implode(' AND ', $where);
374                 $result = db_query($sql);
375                 if (!$result)
376                         return $this->error(_("Cannot update record in ").$this->name);
377
378                 return $result;
379         }
380         //
381         //      Insert record
382         //
383         function insert($data)
384         {
385                 if (!parent::insert_check($data))
386                         return false;
387
388                 $sql = "INSERT INTO ".TB_PREF.$this->name. ' (';
389                 $fields = array();
390                 foreach($data as $fld => $value) {
391                         if (in_array($fld, $this->fields) || (is_array($this->key) ? in_array($this->key) : $fld==$this->key))
392                                 $fields["`$fld`"] = db_escape($value);
393                 }
394                 if (!count($fields))
395                         return $this->error(_("Empty data set for insertion into ".$this->name));
396
397                 $sql .= implode(',', array_keys($fields)) .') VALUES ('. implode(',', $fields).')';
398
399                 $result = db_query($sql);
400                 if (!$result)
401                         return $this->error(_("Cannot insert record into ").$this->name);
402
403                 return $result;
404         }
405
406 }
407
408 /**
409 *
410 *       Data set as array of arrays/objects
411 *
412 *  TODO: default to: fields = ReflectionClass->getProperties
413 **/
414 class array_set extends record_set {
415
416         var $array = array();
417
418         var $object_class;      // name of record object class or null for arrays
419
420         function array_set($name, $fields, $key=null, &$arr=array(), $class = null)
421         {
422                 $this->array = &$arr;
423                 $this->object_class = $class;
424                 $this->record_set($name, $fields, $key);
425         }
426
427         //===========================================================================
428         //      Database functions placeholders
429
430         //
431         //
432         //
433         function get($key=null)
434         {
435                 if ($key===null)
436                         return parent::get();
437
438                 return @$this->array[$key];
439         }
440         //
441         //      Generic list record routine
442         //
443         function get_all()
444         {
445                 return $this->array;
446         }
447
448         function _set_record($data, $record = null)
449         {
450                 if (!isset($record)) {
451                         if ($this->object_class) {
452                                 $record = new $this->object_class;
453                         }
454                         else
455                                 $record = array();
456                 }
457                 foreach(array_merge($this->fields, (array)$this->key) as $n => $fld)
458                 {
459                         if (!is_numeric($n))
460                                 $fld = $n;
461                         if (array_key_exists($fld, $data))
462                         {
463                                 if ($this->object_class)
464                                         $record->$fld = $data[$fld];
465                                 else
466                                         $record[$fld] = $data[$fld];
467                                 $updates = true;
468                         }
469                 }
470                 return $updates ? $record : null;
471         }
472         //
473         //      Generic update record routine
474         //
475         function update($key, $data)
476         {
477                 if (parent::update($key, $data) === false)
478                         return false;
479
480                 $record = $this->_set_record($data, $this->array[$key]);
481                 if (!$record)
482                         return $this->error(_("Empty update data for array ").$this->name);
483
484                 $this->array[$key] = $record;
485
486                 return true;
487         }
488         //
489         //      Delete record
490         //
491         function delete($key)
492         {
493                 if (!parent::delete($key))
494                         return false;
495
496                 unset($this->array[$key]);
497
498                 return true;
499         }
500         //
501         //      Insert record
502         //
503         function insert($data)
504         {
505                 if (parent::insert($data) === false)
506                         return false;
507
508                 $record = $this->_set_record($data);
509                 if (!$record)
510                         return $this->error(_("Empty data for array ").$this->name);
511
512                 $this->array[] = $record;
513
514                 $ret = array_keys($this->array);
515                 return end($ret);
516         }
517 }
518