Switch to actual public/private/protected indicators instead of @access ones
[isso.git] / api.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Blue Static ISSO Framework [#]issoversion[#]
5 || # Copyright ©2002-[#]year[#] 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 [#]gpl[#] 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
24 * api.php
25 *
26 * @package ISSO
27 */
28
29 if (!defined('REQ_AUTO'))
30 {
31 /**
32 * Auto-increasing value
33 */
34 define('REQ_AUTO', -1);
35
36 /**
37 * Set by a cusotm set_*() function
38 */
39 define('REQ_SET', 2);
40
41 /**
42 * Index for cleaning type
43 */
44 define('F_TYPE', 0);
45
46 /**
47 * Index for requirement type
48 */
49 define('F_REQ', 1);
50
51 /**
52 * Index for verification type
53 */
54 define('F_VERIFY', 2);
55
56 /**
57 * Index for relation
58 */
59 define('F_RELATION', 3);
60
61 /**
62 * Relation index for file name, relative to ISSO->apppath
63 */
64 define('F_RELATION_FILE', 0);
65
66 /**
67 * Relation index for class name
68 */
69 define('F_RELATION_CLASS', 1);
70
71 /**
72 * Relation index for field-link alternate name
73 */
74 define('F_RELATION_ALTFIELD', 2);
75 }
76
77 /**
78 * Abstract API
79 *
80 * Abstract class that is used as an API base for most common database interaction
81 * schemes. Creates a simple structure that holds data and can update, delete, and
82 * insert.
83 *
84 * @author Blue Static
85 * @copyright Copyright ©2002 - [#]year[#], Blue Static
86 * @version $Revision$
87 * @package ISSO
88 *
89 */
90 class API
91 {
92 /**
93 * Registry object
94 * @var object
95 */
96 protected $registry = null;
97
98 /**
99 * Fields: used for verification and sanitization
100 * NAME => array(TYPE, REQUIRED, VERIFY METHOD (:self for self-named method), RELATION => array(FILE, CLASS IN FILE, ALTERNATE FIELD NAME))
101 * @var array
102 */
103 protected $fields = array();
104
105 /**
106 * Values array: sanitized and verified field values
107 * @var array
108 */
109 public $values = array();
110
111 /**
112 * Fields that were manually set with set(), not by using set_existing()
113 * @var array
114 */
115 private $setfields = array();
116
117 /**
118 * An array of all of the processed relations on an object
119 * @var array
120 */
121 public $relations = array();
122
123 /**
124 * WHERE condition
125 * @var string
126 */
127 private $condition = '';
128
129 /**
130 * The object table row; a fetched row that represents this instance
131 * @var array
132 */
133 public $objdata = array();
134
135 /**
136 * Insert ID from the insert() command
137 * @var integer
138 */
139 public $insertid = 0;
140
141 /**
142 * Pre- and post-action method stoppers
143 * @var array
144 */
145 public $norunners = array();
146
147 /**
148 * The relations to execute on
149 * @var array
150 */
151 public $dorelations = array('fetch');
152
153 /**
154 * Error list that has been generated
155 * @var array
156 */
157 private $errors = array();
158
159 // ###################################################################
160 /**
161 * Constructor: cannot instantiate class directly
162 */
163 function __construct(&$registry)
164 {
165 if (!is_subclass_of($this, 'API'))
166 {
167 trigger_error('Cannot instantiate the API module directly', E_USER_ERROR);
168 }
169
170 if (!is_object($registry))
171 {
172 trigger_error('The passed registry is not an object', E_USER_ERROR);
173 }
174
175 $this->registry =& $registry;
176 }
177
178 // ###################################################################
179 /**
180 * (PHP 4) Constructor
181 */
182 function API(&$registry)
183 {
184 $this->__construct($registry);
185 }
186
187 // ###################################################################
188 /**
189 * Constructs an error for the error handler to receive
190 *
191 * @access protected
192 *
193 * @param string Error message
194 */
195 function error($message)
196 {
197 $this->errors[] = $message;
198
199 // we want to explicitly specify silence
200 if (APIError() == 'silent')
201 {
202 return;
203 }
204
205 if (!is_callable(APIError()))
206 {
207 trigger_error('No APIError() handler has been set', E_USER_WARNING);
208 }
209
210 call_user_func(APIError(), $message);
211 }
212
213 // ###################################################################
214 /**
215 * Returns the error list. This is because we don't want people mucking
216 * with the error system. It will return an empty array if there are
217 * no errors.
218 *
219 * @access public
220 *
221 * @return array Array of errors
222 */
223 function check_errors()
224 {
225 if (sizeof($this->errors) < 1)
226 {
227 return array();
228 }
229
230 return $this->errors;
231 }
232
233 // ###################################################################
234 /**
235 * Sets a value, sanitizes it, and verifies it
236 *
237 * @access public
238 *
239 * @param string Field name
240 * @param mixed Value
241 * @param bool Do clean?
242 * @param bool Do verify?
243 */
244 function set($field, $value, $doclean = true, $doverify = true)
245 {
246 if (!isset($this->fields["$field"]))
247 {
248 trigger_error('Field `' . $field . '` is not valid', E_USER_WARNING);
249 return;
250 }
251
252 $this->values["$field"] = ($doclean ? $this->registry->clean($value, $this->fields["$field"][F_TYPE]) : $value);
253
254 $this->setfields["$field"] = $field;
255
256 if (isset($this->fields["$field"][F_VERIFY]) AND $doverify)
257 {
258 if ($this->fields["$field"][F_VERIFY] == ':self')
259 {
260 $verify = $this->{"verify_$field"}($field);
261 }
262 else
263 {
264 $verify = $this->{$this->fields["$field"][F_VERIFY]}($field);
265 }
266
267 if ($verify !== true)
268 {
269 if ($verify === false)
270 {
271 $this->error(sprintf($this->registry->modules['localize']->string('Validation of %1$s failed'), $field));
272 }
273 else
274 {
275 $this->error($verify);
276 }
277 }
278 }
279 }
280
281 // ###################################################################
282 /**
283 * Sets the condition to use in the WHERE clause; if not passed, then
284 * it calculates it from the REQ_AUTO field
285 *
286 * @access public
287 *
288 * @param mixed String with WHERE condition; array of fields to use for WHERE builder
289 */
290 function set_condition($condition = '')
291 {
292 if (is_array($condition) AND sizeof($condition) > 0)
293 {
294 $this->condition = '';
295
296 foreach ($condition AS $field)
297 {
298 if (!$this->values["$field"])
299 {
300 trigger_error('The specified field `' . $field . '` for the condition could not be found as it is not set', E_USER_WARNING);
301 continue;
302 }
303
304 $condbits[] = "$field = " . $this->prepare_field_for_sql($field);
305 }
306 $this->condition = implode(' AND ', $condbits);
307 }
308 else if ($condition != '')
309 {
310 $this->condition = $condition;
311 }
312 else
313 {
314 foreach ($this->fields AS $name => $options)
315 {
316 if ($options[F_REQ] == REQ_AUTO)
317 {
318 if (!$this->values["$name"])
319 {
320 trigger_error('Cannot determine condition from the REQ_AUTO field because it is not set', E_USER_WARNING);
321 continue;
322 }
323
324 $this->condition = "$name = " . $this->prepare_field_for_sql($name);
325 }
326 }
327
328 if ($this->condition == '')
329 {
330 trigger_error('No REQ_AUTO fields are present and therefore the condition cannot be created', E_USER_WARNING);
331 }
332 }
333 }
334
335 // ###################################################################
336 /**
337 * Sets existing data into $values where it's not already present
338 *
339 * @access public
340 */
341 function set_existing()
342 {
343 static $run;
344 if ($run)
345 {
346 return;
347 }
348
349 $this->fetch();
350
351 foreach ($this->objdata AS $field => $value)
352 {
353 if (!isset($this->values["$field"]))
354 {
355 $this->values["$field"] = $value;
356 }
357 }
358
359 $run = true;
360 }
361
362 // ###################################################################
363 /**
364 * Fetches a record based on the condition
365 *
366 * @param bool Populate $this->values[] with data, along with $this->objdata[] ?
367 *
368 * @access public
369 */
370 function fetch($populate = false)
371 {
372 if ($this->condition == '')
373 {
374 trigger_error('Condition is empty: cannot fetch', E_USER_ERROR);
375 }
376
377 $this->run_action_method('pre_fetch');
378
379 $result = $this->registry->modules[ISSO_DB_LAYER]->query_first("SELECT * FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
380 if (!$result)
381 {
382 $this->error($this->registry->modules['localize']->string('No records were returned'));
383 return;
384 }
385
386 $this->run_action_method('post_fetch');
387
388 $this->objdata = $result;
389
390 if ($populate)
391 {
392 foreach ($this->objdata AS $key => $value)
393 {
394 if (!isset($this->values["$key"]))
395 {
396 $this->values["$key"] = $value;
397 }
398 }
399 }
400
401 $this->call_relations('fetch');
402 }
403
404 // ###################################################################
405 /**
406 * Inserts a record in the database
407 *
408 * @access public
409 */
410 function insert()
411 {
412 $this->verify();
413
414 $this->run_action_method('pre_insert');
415
416 foreach ($this->setfields AS $field)
417 {
418 $fields[] = $field;
419 $values[] = $this->prepare_field_for_sql($field);
420 }
421
422 $this->registry->modules[ISSO_DB_LAYER]->query("INSERT INTO {$this->prefix}{$this->table} (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ")");
423
424 if (strcasecmp(ISSO_DB_LAYER, 'DB_PostgreSQL') == 0)
425 {
426 foreach ($this->fields AS $field => $info)
427 {
428 if ($info[F_REQ] == REQ_AUTO)
429 {
430 $autofield = $field;
431 break;
432 }
433 }
434
435 $this->insertid = $this->registry->modules[ISSO_DB_LAYER]->insert_id($this->prefix . $this->table, $autofield);
436 }
437 else
438 {
439 $this->insertid = $this->registry->modules[ISSO_DB_LAYER]->insert_id();
440 }
441
442 $this->run_action_method('post_insert');
443 }
444
445 // ###################################################################
446 /**
447 * Updates a record in the database using the data in $vaues
448 *
449 * @access public
450 */
451 function update()
452 {
453 if ($this->condition == '')
454 {
455 trigger_error('Condition is empty: cannot update', E_USER_ERROR);
456 }
457
458 $this->run_action_method('pre_update');
459
460 foreach ($this->setfields AS $field)
461 {
462 $updates[] = "$field = " . $this->prepare_field_for_sql($field);
463 }
464 $updates = implode(', ', $updates);
465
466 $this->registry->modules[ISSO_DB_LAYER]->query("UPDATE {$this->prefix}{$this->table} SET $updates WHERE {$this->condition}");
467
468 $this->run_action_method('post_update');
469 }
470
471 // ###################################################################
472 /**
473 * Deletes a record
474 *
475 * @access public
476 *
477 * @param bool Run API->set_existing()?
478 */
479 function delete($runset = true)
480 {
481 if ($this->condition == '')
482 {
483 trigger_error('Condition is empty: cannot delete', E_USER_ERROR);
484 }
485
486 if ($runset)
487 {
488 $this->set_existing();
489 }
490
491 $this->run_action_method('pre_delete');
492
493 $this->registry->modules[ISSO_DB_LAYER]->query("DELETE FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
494
495 $this->run_action_method('post_delete');
496 }
497
498 // ###################################################################
499 /**
500 * Verifies that all required fields are set
501 *
502 * @access private
503 */
504 function verify()
505 {
506 foreach ($this->fields AS $name => $options)
507 {
508 if ($options[F_REQ] == REQ_YES)
509 {
510 if (!isset($this->values["$name"]))
511 {
512 $this->error(sprintf($this->registry->modules['localize']->string('Required field %1$s was not set'), $name));
513 }
514 }
515 else if ($options[F_REQ] == REQ_SET)
516 {
517 $this->{"set_$name"}();
518 }
519 }
520 }
521
522 // ###################################################################
523 /**
524 * Runs a pre- or post-action method for database commands
525 *
526 * @access private
527 *
528 * @param string Action to run
529 */
530 function run_action_method($method)
531 {
532 if (in_array($method, $this->norunners))
533 {
534 return;
535 }
536
537 $actmethod = (method_exists($this, $method) ? $this->$method() : '');
538 }
539
540 // ###################################################################
541 /**
542 * Determines if it's safe to run a relation; if so, it will return
543 * the WHERE SQL clause
544 *
545 * @access public
546 *
547 * @param string Operation to run
548 */
549 function call_relations($method)
550 {
551 if (!is_array($this->dorelations) OR !in_array($method, $this->dorelations))
552 {
553 return;
554 }
555
556 foreach ($this->fields AS $field => $info)
557 {
558 $value = (isset($this->values["$field"]) ? $this->values["$field"] : $this->objdata["$field"]);
559 if ($value == null OR !is_array($info[F_RELATION]))
560 {
561 continue;
562 }
563
564 if (!file_exists($this->registry->get('apppath') . $info[F_RELATION][F_RELATION_FILE]))
565 {
566 trigger_error("Could not load the relation file for field '$field'");
567 }
568
569 require_once($this->registry->get('apppath') . $info[F_RELATION][F_RELATION_FILE]);
570
571 $this->relations["$field"] = new $info[F_RELATION][F_RELATION_CLASS]($this->registry);
572 $this->relations["$field"]->set(($info[F_RELATION][F_RELATION_ALTFIELD] ? $info[F_RELATION][F_RELATION_ALTFIELD] : $field), $value);
573 $this->relations["$field"]->set_condition();
574 $this->relations["$field"]->$method();
575 }
576 }
577
578 // ###################################################################
579 /**
580 * Prepares a value for use in a SQL query; it encases and escapes
581 * strings and string-like values
582 *
583 * @access private
584 *
585 * @param string Field name
586 *
587 * @return string Prepared value entry
588 */
589 function prepare_field_for_sql($name)
590 {
591 $type = $this->fields["$name"][F_TYPE];
592
593 if ($type == TYPE_NOCLEAN OR $type == TYPE_STR OR $type == TYPE_STRUN)
594 {
595 return "'" . $this->registry->db->escape_string($this->values["$name"]) . "'";
596 }
597 else if ($type == TYPE_BOOL)
598 {
599 return ($this->values["$name"] == true ? "'1'" : "'0'");
600 }
601 else if ($type == TYPE_BIN)
602 {
603 return "'" . $this->registry->db->escape_binary($this->values["$name"]) . "'";
604 }
605 else
606 {
607 return $this->values["$name"];
608 }
609 }
610
611 // ###################################################################
612 /**
613 * Verify field: not a zero value
614 *
615 * @access protected
616 */
617 function verify_nozero($field)
618 {
619 if ($this->values["$field"] == 0)
620 {
621 return sprintf($this->registry->modules['localize']->string('The field "%1$s" cannot be zero'), $field);
622 }
623
624 return true;
625 }
626
627 // ###################################################################
628 /**
629 * Verify field: not empty
630 *
631 * @access protected
632 */
633 function verify_noempty($field)
634 {
635 if (empty($this->values["$field"]))
636 {
637 return sprintf($this->registry->modules['localize']->string('The field "%1$s" cannot be empty'), $field);
638 }
639
640 return true;
641 }
642 }
643
644 // ###################################################################
645 /**
646 * Setter and getter method for the API error reporting system. Passing
647 * a name will cause the set, no arguments will cause the get.
648 *
649 * @access public
650 *
651 * @param mixed Method name in callable form
652 *
653 * @return mixed Method name in callable form
654 */
655 function APIError($new = null)
656 {
657 static $caller, $prev;
658
659 if ($new === -1)
660 {
661 $caller = $prev;
662 }
663 else if ($new !== null)
664 {
665 $prev = $caller;
666 $caller = $new;
667 }
668
669 return $caller;
670 }
671
672 /*=====================================================================*\
673 || ###################################################################
674 || # $HeadURL$
675 || # $Id$
676 || ###################################################################
677 \*=====================================================================*/
678 ?>