Use (c) instead of the actual copyright symbol to avoid the really annoying character...
[isso.git] / Api.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Blue Static ISSO Framework
5 || # Copyright (c)2002-2007 Blue Static
6 || #
7 || # This program is free software; you can redistribute it and/or modify
8 || # it under the terms of the GNU General Public License as published by
9 || # the Free Software Foundation; version 2 of the License.
10 || #
11 || # This program is distributed in the hope that it will be useful, but
12 || # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 || # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 || # more details.
15 || #
16 || # You should have received a copy of the GNU General Public License along
17 || # with this program; if not, write to the Free Software Foundation, Inc.,
18 || # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 || ###################################################################
20 \*=====================================================================*/
21
22 /**
23 * Abstract Datamanger API (api.php)
24 *
25 * @package ISSO
26 */
27
28 if (!defined('REQ_AUTO'))
29 {
30 /**
31 * Yes, required
32 */
33 define('REQ_YES', 1);
34
35 /**
36 * No, not required
37 */
38 define('REQ_NO', 0);
39
40 /**
41 * Auto-increasing value
42 */
43 define('REQ_AUTO', -1);
44
45 /**
46 * Set by a cusotm set_*() function
47 */
48 define('REQ_SET', 2);
49
50 /**
51 * Index for cleaning type
52 */
53 define('F_TYPE', 0);
54
55 /**
56 * Index for requirement type
57 */
58 define('F_REQ', 1);
59
60 /**
61 * Index for verification type
62 */
63 define('F_VERIFY', 2);
64 }
65
66 /**
67 * Abstract API
68 *
69 * Abstract class that is used as an API base for most common database interaction
70 * schemes. Creates a simple structure that holds data and can update, remove, and
71 * insert.
72 *
73 * @author Blue Static
74 * @copyright Copyright (c)2002 - 2007, Blue Static
75 * @package ISSO
76 *
77 */
78 abstract class BSApi
79 {
80 /**
81 * Fields: used for verification and sanitization
82 * NAME => array(TYPE, REQUIRED, VERIFY METHOD (:self for self-named method), RELATION => array(FILE, CLASS IN FILE, ALTERNATE FIELD NAME))
83 * @var array
84 */
85 protected $fields = array();
86
87 /**
88 * Values array: sanitized and verified field values
89 * @var array
90 */
91 public $values = array();
92
93 /**
94 * Fields that were manually set with set(), not by using setExisting()
95 * @var array
96 */
97 private $setfields = array();
98
99 /**
100 * WHERE condition
101 * @var string
102 */
103 private $condition = '';
104
105 /**
106 * The object table row; a fetched row that represents this instance
107 * @var array
108 */
109 public $objdata = array();
110
111 /**
112 * Insert ID from the insert() command
113 * @var integer
114 */
115 public $insertid = 0;
116
117 /**
118 * Error queue that builds up errors
119 * @var array
120 */
121 private $errors = array();
122
123 // ###################################################################
124 /**
125 * Constructor: cannot instantiate class directly
126 */
127 public function __construct()
128 {
129 BSApp::RequiredModules(array('Db', 'Input'));
130 }
131
132 // ###################################################################
133 /**
134 * Adds an error into the error queue that is then hit
135 *
136 * @param string Error message
137 */
138 protected function _error($message)
139 {
140 $this->errors[] = $message;
141 }
142
143 // ###################################################################
144 /**
145 * This runs through all the errors in the error queue and processes
146 * them all at once. Error builders then get a list of all the errors,
147 * and die-by-hit callbacks stop at the first one
148 *
149 * @param string A string param
150 *
151 * @return integer Return value
152 */
153 private function _processErrorQueue()
154 {
155 // we want to explicitly specify silence
156 if (APIError() == 'silent')
157 {
158 return;
159 }
160
161 if (!is_callable(APIError()))
162 {
163 trigger_error('No APIError() handler has been set');
164 }
165
166 foreach ($this->errors AS $e)
167 {
168 call_user_func(APIError(), $e);
169 }
170 }
171
172 // ###################################################################
173 /**
174 * Returns the error list. This is because we don't want people mucking
175 * with the error system. It will return an empty array if there are
176 * no errors.
177 *
178 * @return array Array of errors
179 */
180 public function checkErrors()
181 {
182 if (sizeof($this->errors) < 1)
183 {
184 return array();
185 }
186
187 return $this->errors;
188 }
189
190 // ###################################################################
191 /**
192 * Sets a value, sanitizes it, and verifies it
193 *
194 * @param string Field name
195 * @param mixed Value
196 * @param bool Do clean?
197 * @param bool Do verify?
198 */
199 public function set($field, $value, $doclean = true, $doverify = true)
200 {
201 if (!isset($this->fields["$field"]))
202 {
203 trigger_error('Field "' . $field . '" is not valid');
204 return;
205 }
206
207 $this->values["$field"] = ($doclean ? BSApp::GetType('Input')->clean($value, $this->fields["$field"][F_TYPE]) : $value);
208
209 $this->setfields["$field"] = $field;
210
211 if (isset($this->fields["$field"][F_VERIFY]) AND $doverify)
212 {
213 if ($this->fields["$field"][F_VERIFY] == ':self')
214 {
215 $verify = $this->{"verify_$field"}($field);
216 }
217 else
218 {
219 $verify = $this->{$this->fields["$field"][F_VERIFY]}($field);
220 }
221
222 if ($verify !== true)
223 {
224 if ($verify === false)
225 {
226 $this->_error(sprintf(_('Validation of "%1$s" failed'), $field));
227 }
228 else
229 {
230 $this->_error($verify);
231 }
232 }
233 }
234 }
235
236 // ###################################################################
237 /**
238 * Sets the condition to use in the WHERE clause; if not passed, then
239 * it calculates it from the REQ_AUTO field
240 *
241 * @param mixed String with WHERE condition; array of fields to use for WHERE builder
242 */
243 public function setCondition($condition = '')
244 {
245 if (is_array($condition) AND sizeof($condition) > 0)
246 {
247 $this->condition = '';
248
249 foreach ($condition AS $field)
250 {
251 if (!$this->values["$field"])
252 {
253 trigger_error('The specified field "' . $field . '" for the condition could not be found as it is not set');
254 continue;
255 }
256
257 $condbits[] = "$field = " . $this->_prepareFieldForSql($field);
258 }
259 $this->condition = implode(' AND ', $condbits);
260 }
261 else if ($condition != '')
262 {
263 $this->condition = $condition;
264 }
265 else
266 {
267 foreach ($this->fields AS $name => $options)
268 {
269 if ($options[F_REQ] == REQ_AUTO)
270 {
271 if (!$this->values["$name"])
272 {
273 trigger_error('Cannot determine condition from the REQ_AUTO field because it is not set');
274 continue;
275 }
276
277 $this->condition = "$name = " . $this->_prepareFieldForSql($name);
278 }
279 }
280
281 if ($this->condition == '')
282 {
283 trigger_error('No REQ_AUTO fields are present and therefore the condition cannot be created');
284 }
285 }
286 }
287
288 // ###################################################################
289 /**
290 * Sets existing data into $values where it's not already present
291 */
292 public function setExisting()
293 {
294 static $run;
295 if ($run)
296 {
297 return;
298 }
299
300 $this->fetch();
301
302 foreach ($this->objdata AS $field => $value)
303 {
304 if (!isset($this->values["$field"]))
305 {
306 $this->values["$field"] = $value;
307 }
308 }
309
310 $run = true;
311 }
312
313 // ###################################################################
314 /**
315 * Fetches a record based on the condition
316 *
317 * @param bool Run pre_fetch()?
318 * @param bool Run post_fetch()?
319 *
320 * @return boolean Whether or not the row was successfully fetched
321 */
322 public function fetch($doPre = true, $doPost = true)
323 {
324 if ($this->condition == '')
325 {
326 $this->setCondition();
327 }
328
329 // reset the error queue due to any validation errors caused by fetchable fields
330 $this->errors = array();
331
332 $this->_runActionMethod('pre_fetch', $doPre);
333
334 $result = BSApp::GetType('Db')->queryFirst("SELECT * FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
335 if (!$result)
336 {
337 return false;
338 }
339
340 $this->_runActionMethod('post_fetch', $doPost);
341
342 $this->objdata = $result;
343
344 return true;
345 }
346
347 // ###################################################################
348 /**
349 * Inserts a record in the database
350 *
351 * @param bool Run pre_insert()?
352 * @param bool Run post_insert()?
353 */
354 public function insert($doPre = true, $doPost = true)
355 {
356 $this->verify();
357 $this->_processErrorQueue();
358
359 $this->_runActionMethod('pre_insert', $doPre);
360
361 foreach ($this->setfields AS $field)
362 {
363 $fields[] = $field;
364 $values[] = $this->_prepareFieldForSql($field);
365 }
366
367 BSApp::GetType('Db')->query("INSERT INTO {$this->prefix}{$this->table} (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ")");
368
369 if (BSApp::GetType('DbPostgreSql'))
370 {
371 foreach ($this->fields AS $field => $info)
372 {
373 if ($info[F_REQ] == REQ_AUTO)
374 {
375 $autofield = $field;
376 break;
377 }
378 }
379
380 $this->insertid = BSApp::GetType('Db')->insertId($this->prefix . $this->table, $autofield);
381 }
382 else
383 {
384 $this->insertid = BSApp::GetType('Db')->insertId();
385 }
386
387 $this->_runActionMethod('post_insert', $doPost);
388 }
389
390 // ###################################################################
391 /**
392 * Updates a record in the database using the data in $vaues
393 *
394 * @param bool Run pre_update()?
395 * @param bool Run post_update()?
396 */
397 public function update($doPre = true, $doPost = true)
398 {
399 if ($this->condition == '')
400 {
401 $this->setCondition();
402 }
403
404 $this->_processErrorQueue();
405
406 $this->_runActionMethod('pre_update', $doPre);
407
408 foreach ($this->setfields AS $field)
409 {
410 $updates[] = "$field = " . $this->_prepareFieldForSql($field);
411 }
412 $updates = implode(', ', $updates);
413
414 BSApp::GetType('Db')->query("UPDATE {$this->prefix}{$this->table} SET $updates WHERE {$this->condition}");
415
416 $this->_runActionMethod('post_update', $doPost);
417 }
418
419 // ###################################################################
420 /**
421 * Deletes a record
422 *
423 * @param bool Run pre_remove()?
424 * @param bool Run post_remove()?
425 */
426 public function remove($doPre = true, $doPost = true)
427 {
428 if ($this->condition == '')
429 {
430 $this->setCondition();
431 }
432
433 $this->fetch();
434
435 $this->_runActionMethod('pre_remove', $doPre);
436
437 BSApp::GetType('Db')->query("DELETE FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
438
439 $this->_runActionMethod('post_remove', $doPost);
440 }
441
442 // ###################################################################
443 /**
444 * Verifies that all required fields are set
445 */
446 private function verify()
447 {
448 foreach ($this->fields AS $name => $options)
449 {
450 if ($options[F_REQ] == REQ_YES)
451 {
452 if (!isset($this->values["$name"]))
453 {
454 $this->_error(sprintf(_('The required field "%1$s" was not set'), $name));
455 }
456 }
457 else if ($options[F_REQ] == REQ_SET)
458 {
459 $this->{"set_$name"}();
460 }
461 }
462 }
463
464 // ###################################################################
465 /**
466 * Runs a pre- or post-action method for database commands
467 *
468 * @param string Action to run
469 * @param bool Actually run it?
470 */
471 private function _runActionMethod($method, $doRun)
472 {
473 if (!$doRun)
474 {
475 return;
476 }
477
478 $actmethod = (method_exists($this, $method) ? $this->$method() : '');
479 }
480
481 // ###################################################################
482 /**
483 * Prepares a value for use in a SQL query; it encases and escapes
484 * strings and string-like values
485 *
486 * @param string Field name
487 *
488 * @return string Prepared value entry
489 */
490 private function _prepareFieldForSql($name)
491 {
492 $type = $this->fields["$name"][F_TYPE];
493
494 if ($type == TYPE_NONE OR $type == TYPE_STR OR $type == TYPE_STRUN)
495 {
496 return "'" . BSApp::GetType('Db')->escapeString($this->values["$name"]) . "'";
497 }
498 else if ($type == TYPE_BOOL)
499 {
500 return ($this->values["$name"] == true ? "'1'" : "'0'");
501 }
502 else if ($type == TYPE_BIN)
503 {
504 return "'" . BSApp::GetType('Db')->escapeBinary($this->values["$name"]) . "'";
505 }
506 else
507 {
508 return $this->values["$name"];
509 }
510 }
511
512 // ###################################################################
513 /**
514 * Verify field: not a zero value
515 */
516 protected function verify_nozero($field)
517 {
518 if ($this->values["$field"] == 0)
519 {
520 return sprintf(_('The field "%1$s" cannot be zero'), $field);
521 }
522
523 return true;
524 }
525
526 // ###################################################################
527 /**
528 * Verify field: not empty
529 */
530 protected function verify_noempty($field)
531 {
532 if (empty($this->values["$field"]))
533 {
534 return sprintf(_('The field "%1$s" cannot be empty'), $field);
535 }
536
537 return true;
538 }
539
540 // ###################################################################
541 /**
542 * Verify field: unique
543 */
544 protected function verify_unique($field)
545 {
546 $res = BSApp::GetType('Db')->queryFirst("SELECT $field FROM {$this->prefix}{$this->table} WHERE $field = " . $this->_prepareFieldForSql($field) . (empty($this->condition) ? "" : " AND !({$this->condition})"));
547 if ($res)
548 {
549 return sprintf(_('The "%1$s" field must contain a unique value'), $field);
550 }
551
552 return true;
553 }
554 }
555
556 // ###################################################################
557 /**
558 * Setter and getter method for the API error reporting system. Passing
559 * a name will cause the set, no arguments will cause the get.
560 *
561 * @param mixed Method name in callable form
562 *
563 * @return mixed Method name in callable form
564 */
565 function APIError($new = null)
566 {
567 static $caller, $prev;
568
569 if ($new === -1)
570 {
571 $caller = $prev;
572 }
573 else if ($new !== null)
574 {
575 $prev = $caller;
576 $caller = $new;
577 }
578
579 return $caller;
580 }
581
582 ?>