Update version.php to 3.3.0
[isso.git] / Api.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Blue Static ISSO Framework
5 || # Copyright (c)2005-2009 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 /**
62 * Abstract API
63 *
64 * Abstract class that is used as an API base for most common database interaction
65 * schemes. Creates a simple structure that holds data and can update, delete, and
66 * insert.
67 *
68 * Life-cycle of a new object:
69 * 1. $o = new SubApi();
70 * 2. $o->set('foo', 'abc');
71 * 3. $o->set('test', 45);
72 * 4. try { $o->insert(); <other actions that depend on the saved record> } catch (ApiException $e) {}
73 *
74 * @author Blue Static
75 * @copyright Copyright (c)2005 - 2009, Blue Static
76 * @package ISSO
77 *
78 */
79 abstract class BSApi
80 {
81 /**
82 * Fields: used for verification and sanitization
83 * NAME => array(TYPE, REQUIRED)
84 * @var array
85 */
86 protected $fields = array();
87
88 /**
89 * The table name the API maps objects for
90 * @var string
91 */
92 protected $table = '___UNDEFINED___';
93
94 /**
95 * The database table prefix
96 * @var string
97 */
98 protected $prefix = '';
99
100 /**
101 * Values array: sanitized and validated field values
102 * @var array
103 */
104 public $values = array();
105
106 /**
107 * Fields that were set by the client
108 * @var array
109 */
110 private $setfields = array();
111
112 /**
113 * WHERE condition
114 * @var string
115 */
116 protected $condition = '';
117
118 /**
119 * The object table row; a fetched row that represents this instance
120 * @var array
121 */
122 public $record = array();
123
124 /**
125 * Insert ID from the insert() command
126 * @var integer
127 */
128 public $insertid = 0;
129
130 /**
131 * Error queue that builds up errors
132 * @var ApiException
133 */
134 private $exception = null;
135
136 /**
137 * Constructor
138 */
139 public function __construct()
140 {
141 if (!BSApp::$input instanceof BSInput)
142 {
143 throw new Exception('BSApp::$input is not an instance of BSInput');
144 }
145 if (!BSApp::$db instanceof BSDb)
146 {
147 throw new Exception('BSApp::$db is not an instance of BSDb');
148 }
149 }
150
151 /**
152 * Adds an error into the error queue that is then hit
153 *
154 * @param Exception Error message
155 */
156 protected function _error(Exception $e)
157 {
158 if ($this->exception == null)
159 {
160 $this->exception = new ApiException();
161 }
162 $this->exception->addException($e);
163 }
164
165 /**
166 * This simply throws the ApiException if it exists, which inside holds
167 * all of the individual and specific errors
168 */
169 protected function _processErrorQueue()
170 {
171 if ($this->exception)
172 {
173 throw $this->exception;
174 }
175 }
176
177 /**
178 * Sets an array of data into the API, ignoring things in $exclude. Keys
179 * in the array that don't exist in the API will be ignored.
180 *
181 * @param array A dictionary of field names and values to set
182 * @param array Array of keys to exclude
183 */
184 public function setArray(Array $data, $exclude = array())
185 {
186 foreach ($data as $key => $value)
187 {
188 if (in_array($key, $exclude) || !isset($this->fields[$key]))
189 {
190 continue;
191 }
192 $this->set($key, $value);
193 }
194 }
195
196 /**
197 * Resets the API object to an initial state. This will NOT clear the primary (REQ_AUTO)
198 * field.
199 */
200 public function reset()
201 {
202 foreach ($this->fields as $field => $settings)
203 {
204 if ($settings[F_REQ] == REQ_AUTO)
205 {
206 $savename = $field;
207 $savevalue = $this->fetchValue($field);
208 $savevalue = ($savevalue ? $savevalue : $this->insertid);
209 break;
210 }
211 }
212
213 $this->setfields = array();
214 $this->values = array();
215 $this->condition = '';
216 $this->insertid = 0;
217 $this->exception = null;
218
219 $this->set($savename, $savevalue);
220 }
221
222 /**
223 * Sets a value, sanitizes it, and validates it
224 *
225 * @param string Field name
226 * @param mixed Value
227 * @param bool Do clean?
228 * @param bool Do validation?
229 */
230 public function set($field, $value, $doclean = true, $dovalidate = true)
231 {
232 if (!isset($this->fields["$field"]))
233 {
234 throw new Exception('Field "' . $field . '" is not valid');
235 }
236
237 $this->values["$field"] = ($doclean ? BSApp::$input->clean($value, $this->fields["$field"][F_TYPE]) : $value);
238
239 $this->setfields["$field"] = $field;
240
241 if ($dovalidate && method_exists($this, "validate_$field"))
242 {
243 $this->{"validate_$field"}($field);
244 }
245 }
246
247 /**
248 * Sets the condition to use in the WHERE clause; if not passed, then
249 * it calculates it from the REQ_AUTO field
250 *
251 * @param mixed String with WHERE condition; array of fields to use for WHERE builder
252 */
253 public function setCondition($condition = null)
254 {
255 if (is_array($condition) && sizeof($condition) > 0)
256 {
257 $this->condition = '';
258
259 foreach ($condition as $field)
260 {
261 if (!$this->values["$field"])
262 {
263 throw new Exception('The specified field "' . $field . '" for the condition could not be found as it is not set');
264 }
265
266 $condbits[] = "$field = " . $this->_prepareFieldForSql($field);
267 }
268 $this->condition = implode(' AND ', $condbits);
269 }
270 else if ($condition)
271 {
272 $this->condition = $condition;
273 }
274 else
275 {
276 foreach ($this->fields as $name => $options)
277 {
278 if ($options[F_REQ] == REQ_AUTO)
279 {
280 if (!$this->values["$name"])
281 {
282 throw new Exception('Cannot determine condition from the REQ_AUTO field because it is not set');
283 }
284
285 $this->condition = "$name = " . $this->_prepareFieldForSql($name);
286 }
287 }
288
289 if (!$this->condition)
290 {
291 throw new Exception('No REQ_AUTO fields are present and therefore the condition cannot be created');
292 }
293 }
294 }
295
296 /**
297 * Fetches a record based on the condition
298 *
299 * @param bool Run pre_fetch()?
300 * @param bool Run post_fetch()?
301 *
302 * @return boolean Whether or not the row was successfully fetched
303 */
304 public function fetch($doPre = true, $doPost = true)
305 {
306 if (!$this->condition)
307 {
308 $this->setCondition();
309 }
310
311 // reset the error queue due to any validation errors caused by fetchable fields
312 $this->errors = null;
313
314 $this->_runActionMethod('pre_fetch', $doPre);
315
316 $result = BSApp::$db->queryFirst("SELECT * FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
317 if (!$result)
318 {
319 return false;
320 }
321
322 $this->_runActionMethod('post_fetch', $doPost);
323
324 $this->record = $result;
325
326 return true;
327 }
328
329 /**
330 * Inserts a record in the database
331 *
332 * @param bool Run pre_insert()?
333 * @param bool Run post_insert()?
334 */
335 public function insert($doPre = true, $doPost = true)
336 {
337 $this->_verifyRequired();
338 $this->_processErrorQueue();
339
340 $this->_runActionMethod('pre_insert', $doPre);
341
342 $fields = $values = array();
343 foreach ($this->setfields as $field)
344 {
345 $fields[] = $field;
346 $values[] = $this->_prepareFieldForSql($field);
347 }
348
349 BSApp::$db->query("INSERT INTO {$this->prefix}{$this->table} (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ")");
350
351 if (BSApp::$db instanceof BSDbPostgreSql)
352 {
353 foreach ($this->fields as $field => $info)
354 {
355 if ($info[F_REQ] == REQ_AUTO)
356 {
357 $autofield = $field;
358 break;
359 }
360 }
361
362 $this->insertid = BSApp::$db->insertId($this->prefix . $this->table, $autofield);
363 }
364 else
365 {
366 $this->insertid = BSApp::$db->insertId();
367 }
368
369 $this->_runActionMethod('post_insert', $doPost);
370 }
371
372 /**
373 * Updates a record in the database using the data in $vaues
374 *
375 * @param bool Run pre_update()?
376 * @param bool Run post_update()?
377 */
378 public function update($doPre = true, $doPost = true)
379 {
380 if (!$this->condition)
381 {
382 $this->setCondition();
383 }
384
385 $this->_processErrorQueue();
386
387 $this->_runActionMethod('pre_update', $doPre);
388
389 foreach ($this->setfields as $field)
390 {
391 $updates[] = "$field = " . $this->_prepareFieldForSql($field);
392 }
393 $updates = implode(', ', $updates);
394
395 BSApp::$db->query("UPDATE {$this->prefix}{$this->table} SET $updates WHERE {$this->condition}");
396
397 $this->_runActionMethod('post_update', $doPost);
398 }
399
400 /**
401 * Deletes a record
402 *
403 * @param bool Run pre_delete()?
404 * @param bool Run post_delete()?
405 */
406 public function delete($doPre = true, $doPost = true)
407 {
408 if (!$this->condition)
409 {
410 $this->setCondition();
411 }
412
413 $this->fetch();
414
415 $this->_runActionMethod('pre_delete', $doPre);
416
417 BSApp::$db->query("DELETE FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
418
419 $this->_runActionMethod('post_delete', $doPost);
420 }
421
422 /**
423 * Verifies that all required fields are set
424 */
425 protected function _verifyRequired()
426 {
427 foreach ($this->fields as $name => $options)
428 {
429 if ($options[F_REQ] == REQ_YES)
430 {
431 if (!isset($this->values["$name"]))
432 {
433 $this->_error(new FieldException(sprintf(_('The required field "%1$s" was not set'), $name), $name));
434 }
435 }
436 else if ($options[F_REQ] == REQ_SET)
437 {
438 $this->{"set_$name"}();
439 }
440 }
441 }
442
443 /**
444 * Runs a pre- or post-action method for database commands
445 *
446 * @param string Action to run
447 * @param bool Actually run it?
448 */
449 protected function _runActionMethod($method, $doRun)
450 {
451 if (!$doRun || !method_exists($this, $method))
452 {
453 return;
454 }
455
456 $this->$method();
457 }
458
459 /**
460 * Prepares a value for use in a SQL query; it encases and escapes
461 * strings and string-like values
462 *
463 * @param string Field name
464 *
465 * @return string Prepared value entry
466 */
467 protected function _prepareFieldForSql($name)
468 {
469 $type = $this->fields["$name"][F_TYPE];
470
471 if ($type == TYPE_NONE || $type == TYPE_STR || $type == TYPE_STRUN)
472 {
473 return "'" . BSApp::$db->escapeString($this->values["$name"]) . "'";
474 }
475 else if ($type == TYPE_BOOL)
476 {
477 return ($this->values["$name"] == true ? "'1'" : "'0'");
478 }
479 else if ($type == TYPE_BIN)
480 {
481 return "'" . BSApp::$db->escapeBinary($this->values["$name"]) . "'";
482 }
483 else
484 {
485 return $this->values["$name"];
486 }
487 }
488
489 /**
490 * Determines the value of a field from Api->record[], Api->values[] (in that order)
491 *
492 * @param string The field ID to determine for
493 *
494 * @return mixed
495 */
496 public function fetchValue($field)
497 {
498 if ($this->record[$field])
499 {
500 return $this->record[$field];
501 }
502 else if ($this->values[$field])
503 {
504 return $this->values[$field];
505 }
506
507 return null;
508 }
509
510 /**
511 * Verify field: not a zero value
512 */
513 protected function _verifyIsNotZero($field)
514 {
515 if ($this->values[$field] == 0)
516 {
517 $this->_error(new FieldException(sprintf(_('The field "%1$s" cannot be zero'), $field), $field));
518 return false;
519 }
520
521 return true;
522 }
523
524 /**
525 * Verify field: not empty
526 */
527 protected function _verifyIsNotEmpty($field)
528 {
529 if (empty($this->values[$field]))
530 {
531 $this->_error(new FieldException(sprintf(_('The field "%1$s" cannot be empty'), $field), $field));
532 return false;
533 }
534
535 return true;
536 }
537
538 /**
539 * Verify field: unique
540 */
541 protected function _verifyIsNotUnique($field)
542 {
543 $res = BSApp::$db->queryFirst("SELECT $field FROM {$this->prefix}{$this->table} WHERE $field = " . $this->_prepareFieldForSql($field) . (empty($this->condition) ? "" : " AND !({$this->condition})"));
544 if ($res)
545 {
546 $this->_error(new FieldException(sprintf(_('The "%1$s" field must contain a unique value'), $field), $field));
547 return false;
548 }
549
550 return true;
551 }
552 }
553
554 /**
555 * API Exception
556 *
557 * This class is an exception container that can be used to store a series
558 * of exceptions that can be thrown as one
559 *
560 * @author rsesek
561 * @copyright Copyright (c)2005 - 2009, Blue Static
562 * @package ISSO
563 *
564 */
565 class ApiException extends Exception
566 {
567 /**
568 * Array of exceptions
569 * @var array
570 */
571 private $exceptions = array();
572
573 /**
574 * Constructor
575 */
576 public function __construct()
577 {
578 parent::__construct(_('An error occurred while processing the API data. Errors: '));
579 }
580
581 /**
582 * Adds an exception to the collection
583 *
584 * @param Exception $e
585 */
586 public function addException(Exception $e)
587 {
588 $this->exceptions[] = $e;
589 $this->message .= ' (' . sizeof($this->exceptions) . ') ' . $e->getMessage();
590 }
591
592 /**
593 * Returns an array of all the exceptions in the collection
594 *
595 * @return array
596 */
597 public function getExceptions()
598 {
599 return $this->exceptions;
600 }
601 }
602
603 /**
604 * Field Exception
605 *
606 * This exception represents a problem with an API field
607 *
608 * @author rsesek
609 * @copyright Copyright (c)2005 - 2009, Blue Static
610 * @package ISSO
611 *
612 */
613 class FieldException extends Exception
614 {
615 /**
616 * The name of the erroring field
617 * @var string
618 */
619 private $field;
620
621 /**
622 * Constructor: create a new exception
623 */
624 public function __construct($error, $field)
625 {
626 $this->field = $field;
627 parent::__construct($error);
628 }
629
630 /**
631 * Returns the name of the field the exception is for
632 *
633 * @return string
634 */
635 public function getField()
636 {
637 return $this->field;
638 }
639 }
640
641 ?>