Removing the PHP4 constructors
[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 * Constructs an error for the error handler to receive
181 *
182 * @access protected
183 *
184 * @param string Error message
185 */
186 function error($message)
187 {
188 $this->errors[] = $message;
189
190 // we want to explicitly specify silence
191 if (APIError() == 'silent')
192 {
193 return;
194 }
195
196 if (!is_callable(APIError()))
197 {
198 trigger_error('No APIError() handler has been set', E_USER_WARNING);
199 }
200
201 call_user_func(APIError(), $message);
202 }
203
204 // ###################################################################
205 /**
206 * Returns the error list. This is because we don't want people mucking
207 * with the error system. It will return an empty array if there are
208 * no errors.
209 *
210 * @access public
211 *
212 * @return array Array of errors
213 */
214 function check_errors()
215 {
216 if (sizeof($this->errors) < 1)
217 {
218 return array();
219 }
220
221 return $this->errors;
222 }
223
224 // ###################################################################
225 /**
226 * Sets a value, sanitizes it, and verifies it
227 *
228 * @access public
229 *
230 * @param string Field name
231 * @param mixed Value
232 * @param bool Do clean?
233 * @param bool Do verify?
234 */
235 function set($field, $value, $doclean = true, $doverify = true)
236 {
237 if (!isset($this->fields["$field"]))
238 {
239 trigger_error('Field `' . $field . '` is not valid', E_USER_WARNING);
240 return;
241 }
242
243 $this->values["$field"] = ($doclean ? $this->registry->clean($value, $this->fields["$field"][F_TYPE]) : $value);
244
245 $this->setfields["$field"] = $field;
246
247 if (isset($this->fields["$field"][F_VERIFY]) AND $doverify)
248 {
249 if ($this->fields["$field"][F_VERIFY] == ':self')
250 {
251 $verify = $this->{"verify_$field"}($field);
252 }
253 else
254 {
255 $verify = $this->{$this->fields["$field"][F_VERIFY]}($field);
256 }
257
258 if ($verify !== true)
259 {
260 if ($verify === false)
261 {
262 $this->error(sprintf($this->registry->modules['localize']->string('Validation of %1$s failed'), $field));
263 }
264 else
265 {
266 $this->error($verify);
267 }
268 }
269 }
270 }
271
272 // ###################################################################
273 /**
274 * Sets the condition to use in the WHERE clause; if not passed, then
275 * it calculates it from the REQ_AUTO field
276 *
277 * @access public
278 *
279 * @param mixed String with WHERE condition; array of fields to use for WHERE builder
280 */
281 function set_condition($condition = '')
282 {
283 if (is_array($condition) AND sizeof($condition) > 0)
284 {
285 $this->condition = '';
286
287 foreach ($condition AS $field)
288 {
289 if (!$this->values["$field"])
290 {
291 trigger_error('The specified field `' . $field . '` for the condition could not be found as it is not set', E_USER_WARNING);
292 continue;
293 }
294
295 $condbits[] = "$field = " . $this->prepare_field_for_sql($field);
296 }
297 $this->condition = implode(' AND ', $condbits);
298 }
299 else if ($condition != '')
300 {
301 $this->condition = $condition;
302 }
303 else
304 {
305 foreach ($this->fields AS $name => $options)
306 {
307 if ($options[F_REQ] == REQ_AUTO)
308 {
309 if (!$this->values["$name"])
310 {
311 trigger_error('Cannot determine condition from the REQ_AUTO field because it is not set', E_USER_WARNING);
312 continue;
313 }
314
315 $this->condition = "$name = " . $this->prepare_field_for_sql($name);
316 }
317 }
318
319 if ($this->condition == '')
320 {
321 trigger_error('No REQ_AUTO fields are present and therefore the condition cannot be created', E_USER_WARNING);
322 }
323 }
324 }
325
326 // ###################################################################
327 /**
328 * Sets existing data into $values where it's not already present
329 *
330 * @access public
331 */
332 function set_existing()
333 {
334 static $run;
335 if ($run)
336 {
337 return;
338 }
339
340 $this->fetch();
341
342 foreach ($this->objdata AS $field => $value)
343 {
344 if (!isset($this->values["$field"]))
345 {
346 $this->values["$field"] = $value;
347 }
348 }
349
350 $run = true;
351 }
352
353 // ###################################################################
354 /**
355 * Fetches a record based on the condition
356 *
357 * @param bool Populate $this->values[] with data, along with $this->objdata[] ?
358 *
359 * @access public
360 */
361 function fetch($populate = false)
362 {
363 if ($this->condition == '')
364 {
365 trigger_error('Condition is empty: cannot fetch', E_USER_ERROR);
366 }
367
368 $this->run_action_method('pre_fetch');
369
370 $result = $this->registry->modules[ISSO_DB_LAYER]->query_first("SELECT * FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
371 if (!$result)
372 {
373 $this->error($this->registry->modules['localize']->string('No records were returned'));
374 return;
375 }
376
377 $this->run_action_method('post_fetch');
378
379 $this->objdata = $result;
380
381 if ($populate)
382 {
383 foreach ($this->objdata AS $key => $value)
384 {
385 if (!isset($this->values["$key"]))
386 {
387 $this->values["$key"] = $value;
388 }
389 }
390 }
391
392 $this->call_relations('fetch');
393 }
394
395 // ###################################################################
396 /**
397 * Inserts a record in the database
398 *
399 * @access public
400 */
401 function insert()
402 {
403 $this->verify();
404
405 $this->run_action_method('pre_insert');
406
407 foreach ($this->setfields AS $field)
408 {
409 $fields[] = $field;
410 $values[] = $this->prepare_field_for_sql($field);
411 }
412
413 $this->registry->modules[ISSO_DB_LAYER]->query("INSERT INTO {$this->prefix}{$this->table} (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ")");
414
415 if (strcasecmp(ISSO_DB_LAYER, 'DB_PostgreSQL') == 0)
416 {
417 foreach ($this->fields AS $field => $info)
418 {
419 if ($info[F_REQ] == REQ_AUTO)
420 {
421 $autofield = $field;
422 break;
423 }
424 }
425
426 $this->insertid = $this->registry->modules[ISSO_DB_LAYER]->insert_id($this->prefix . $this->table, $autofield);
427 }
428 else
429 {
430 $this->insertid = $this->registry->modules[ISSO_DB_LAYER]->insert_id();
431 }
432
433 $this->run_action_method('post_insert');
434 }
435
436 // ###################################################################
437 /**
438 * Updates a record in the database using the data in $vaues
439 *
440 * @access public
441 */
442 function update()
443 {
444 if ($this->condition == '')
445 {
446 trigger_error('Condition is empty: cannot update', E_USER_ERROR);
447 }
448
449 $this->run_action_method('pre_update');
450
451 foreach ($this->setfields AS $field)
452 {
453 $updates[] = "$field = " . $this->prepare_field_for_sql($field);
454 }
455 $updates = implode(', ', $updates);
456
457 $this->registry->modules[ISSO_DB_LAYER]->query("UPDATE {$this->prefix}{$this->table} SET $updates WHERE {$this->condition}");
458
459 $this->run_action_method('post_update');
460 }
461
462 // ###################################################################
463 /**
464 * Deletes a record
465 *
466 * @access public
467 *
468 * @param bool Run API->set_existing()?
469 */
470 function delete($runset = true)
471 {
472 if ($this->condition == '')
473 {
474 trigger_error('Condition is empty: cannot delete', E_USER_ERROR);
475 }
476
477 if ($runset)
478 {
479 $this->set_existing();
480 }
481
482 $this->run_action_method('pre_delete');
483
484 $this->registry->modules[ISSO_DB_LAYER]->query("DELETE FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
485
486 $this->run_action_method('post_delete');
487 }
488
489 // ###################################################################
490 /**
491 * Verifies that all required fields are set
492 *
493 * @access private
494 */
495 function verify()
496 {
497 foreach ($this->fields AS $name => $options)
498 {
499 if ($options[F_REQ] == REQ_YES)
500 {
501 if (!isset($this->values["$name"]))
502 {
503 $this->error(sprintf($this->registry->modules['localize']->string('Required field %1$s was not set'), $name));
504 }
505 }
506 else if ($options[F_REQ] == REQ_SET)
507 {
508 $this->{"set_$name"}();
509 }
510 }
511 }
512
513 // ###################################################################
514 /**
515 * Runs a pre- or post-action method for database commands
516 *
517 * @access private
518 *
519 * @param string Action to run
520 */
521 function run_action_method($method)
522 {
523 if (in_array($method, $this->norunners))
524 {
525 return;
526 }
527
528 $actmethod = (method_exists($this, $method) ? $this->$method() : '');
529 }
530
531 // ###################################################################
532 /**
533 * Determines if it's safe to run a relation; if so, it will return
534 * the WHERE SQL clause
535 *
536 * @access public
537 *
538 * @param string Operation to run
539 */
540 function call_relations($method)
541 {
542 if (!is_array($this->dorelations) OR !in_array($method, $this->dorelations))
543 {
544 return;
545 }
546
547 foreach ($this->fields AS $field => $info)
548 {
549 $value = (isset($this->values["$field"]) ? $this->values["$field"] : $this->objdata["$field"]);
550 if ($value == null OR !is_array($info[F_RELATION]))
551 {
552 continue;
553 }
554
555 if (!file_exists($this->registry->get('apppath') . $info[F_RELATION][F_RELATION_FILE]))
556 {
557 trigger_error("Could not load the relation file for field '$field'");
558 }
559
560 require_once($this->registry->get('apppath') . $info[F_RELATION][F_RELATION_FILE]);
561
562 $this->relations["$field"] = new $info[F_RELATION][F_RELATION_CLASS]($this->registry);
563 $this->relations["$field"]->set(($info[F_RELATION][F_RELATION_ALTFIELD] ? $info[F_RELATION][F_RELATION_ALTFIELD] : $field), $value);
564 $this->relations["$field"]->set_condition();
565 $this->relations["$field"]->$method();
566 }
567 }
568
569 // ###################################################################
570 /**
571 * Prepares a value for use in a SQL query; it encases and escapes
572 * strings and string-like values
573 *
574 * @access private
575 *
576 * @param string Field name
577 *
578 * @return string Prepared value entry
579 */
580 function prepare_field_for_sql($name)
581 {
582 $type = $this->fields["$name"][F_TYPE];
583
584 if ($type == TYPE_NOCLEAN OR $type == TYPE_STR OR $type == TYPE_STRUN)
585 {
586 return "'" . $this->registry->db->escape_string($this->values["$name"]) . "'";
587 }
588 else if ($type == TYPE_BOOL)
589 {
590 return ($this->values["$name"] == true ? "'1'" : "'0'");
591 }
592 else if ($type == TYPE_BIN)
593 {
594 return "'" . $this->registry->db->escape_binary($this->values["$name"]) . "'";
595 }
596 else
597 {
598 return $this->values["$name"];
599 }
600 }
601
602 // ###################################################################
603 /**
604 * Verify field: not a zero value
605 *
606 * @access protected
607 */
608 function verify_nozero($field)
609 {
610 if ($this->values["$field"] == 0)
611 {
612 return sprintf($this->registry->modules['localize']->string('The field "%1$s" cannot be zero'), $field);
613 }
614
615 return true;
616 }
617
618 // ###################################################################
619 /**
620 * Verify field: not empty
621 *
622 * @access protected
623 */
624 function verify_noempty($field)
625 {
626 if (empty($this->values["$field"]))
627 {
628 return sprintf($this->registry->modules['localize']->string('The field "%1$s" cannot be empty'), $field);
629 }
630
631 return true;
632 }
633 }
634
635 // ###################################################################
636 /**
637 * Setter and getter method for the API error reporting system. Passing
638 * a name will cause the set, no arguments will cause the get.
639 *
640 * @access public
641 *
642 * @param mixed Method name in callable form
643 *
644 * @return mixed Method name in callable form
645 */
646 function APIError($new = null)
647 {
648 static $caller, $prev;
649
650 if ($new === -1)
651 {
652 $caller = $prev;
653 }
654 else if ($new !== null)
655 {
656 $prev = $caller;
657 $caller = $new;
658 }
659
660 return $caller;
661 }
662
663 /*=====================================================================*\
664 || ###################################################################
665 || # $HeadURL$
666 || # $Id$
667 || ###################################################################
668 \*=====================================================================*/
669 ?>