These two files need to be swapped
[isso.git] / Api.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Blue Static ISSO Framework
5 || # Copyright (c)2005-2008 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, remove, 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 - 2008, 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 /**
153 * Adds an error into the error queue that is then hit
154 *
155 * @param Exception Error message
156 */
157 protected function _error(Exception $e)
158 {
159 if ($this->exception == null)
160 {
161 $this->exception = new ApiException();
162 }
163 $this->exception->addException($e);
164 }
165
166 // ###################################################################
167 /**
168 * This simply throws the ApiException if it exists, which inside holds
169 * all of the individual and specific errors
170 */
171 private function _processErrorQueue()
172 {
173 if ($this->exception)
174 {
175 throw $this->exception;
176 }
177 }
178
179 // ###################################################################
180 /**
181 * Sets a value, sanitizes it, and validates it
182 *
183 * @param string Field name
184 * @param mixed Value
185 * @param bool Do clean?
186 * @param bool Do validation?
187 */
188 public function set($field, $value, $doclean = true, $dovalidate = true)
189 {
190 if (!isset($this->fields["$field"]))
191 {
192 throw new Exception('Field "' . $field . '" is not valid');
193 }
194
195 $this->values["$field"] = ($doclean ? BSApp::$input->clean($value, $this->fields["$field"][F_TYPE]) : $value);
196
197 $this->setfields["$field"] = $field;
198
199 if ($dovalidate && method_exists($this, "validate_$field"))
200 {
201 $this->{"validate_$field"}($field);
202 }
203 }
204
205 // ###################################################################
206 /**
207 * Sets the condition to use in the WHERE clause; if not passed, then
208 * it calculates it from the REQ_AUTO field
209 *
210 * @param mixed String with WHERE condition; array of fields to use for WHERE builder
211 */
212 public function setCondition($condition = null)
213 {
214 if (is_array($condition) && sizeof($condition) > 0)
215 {
216 $this->condition = '';
217
218 foreach ($condition as $field)
219 {
220 if (!$this->values["$field"])
221 {
222 throw new Exception('The specified field "' . $field . '" for the condition could not be found as it is not set');
223 }
224
225 $condbits[] = "$field = " . $this->_prepareFieldForSql($field);
226 }
227 $this->condition = implode(' AND ', $condbits);
228 }
229 else if ($condition)
230 {
231 $this->condition = $condition;
232 }
233 else
234 {
235 foreach ($this->fields as $name => $options)
236 {
237 if ($options[F_REQ] == REQ_AUTO)
238 {
239 if (!$this->values["$name"])
240 {
241 throw new Exception('Cannot determine condition from the REQ_AUTO field because it is not set');
242 }
243
244 $this->condition = "$name = " . $this->_prepareFieldForSql($name);
245 }
246 }
247
248 if (!$this->condition)
249 {
250 throw new Exception('No REQ_AUTO fields are present and therefore the condition cannot be created');
251 }
252 }
253 }
254
255 // ###################################################################
256 /**
257 * Fetches a record based on the condition
258 *
259 * @param bool Run pre_fetch()?
260 * @param bool Run post_fetch()?
261 *
262 * @return boolean Whether or not the row was successfully fetched
263 */
264 public function fetch($doPre = true, $doPost = true)
265 {
266 if (!$this->condition)
267 {
268 $this->setCondition();
269 }
270
271 // reset the error queue due to any validation errors caused by fetchable fields
272 $this->errors = null;
273
274 $this->_runActionMethod('pre_fetch', $doPre);
275
276 $result = BSApp::$db->queryFirst("SELECT * FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
277 if (!$result)
278 {
279 return false;
280 }
281
282 $this->_runActionMethod('post_fetch', $doPost);
283
284 $this->record = $result;
285
286 return true;
287 }
288
289 // ###################################################################
290 /**
291 * Inserts a record in the database
292 *
293 * @param bool Run pre_insert()?
294 * @param bool Run post_insert()?
295 */
296 public function insert($doPre = true, $doPost = true)
297 {
298 $this->_verifyRequired();
299 $this->_processErrorQueue();
300
301 $this->_runActionMethod('pre_insert', $doPre);
302
303 $fields = $values = array();
304 foreach ($this->setfields as $field)
305 {
306 $fields[] = $field;
307 $values[] = $this->_prepareFieldForSql($field);
308 }
309
310 BSApp::$db->query("INSERT INTO {$this->prefix}{$this->table} (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ")");
311
312 if (BSApp::$db instanceof BSDbPostgreSql)
313 {
314 foreach ($this->fields as $field => $info)
315 {
316 if ($info[F_REQ] == REQ_AUTO)
317 {
318 $autofield = $field;
319 break;
320 }
321 }
322
323 $this->insertid = BSApp::$db->insertId($this->prefix . $this->table, $autofield);
324 }
325 else
326 {
327 $this->insertid = BSApp::$db->insertId();
328 }
329
330 $this->_runActionMethod('post_insert', $doPost);
331 }
332
333 // ###################################################################
334 /**
335 * Updates a record in the database using the data in $vaues
336 *
337 * @param bool Run pre_update()?
338 * @param bool Run post_update()?
339 */
340 public function update($doPre = true, $doPost = true)
341 {
342 if (!$this->condition)
343 {
344 $this->setCondition();
345 }
346
347 $this->_processErrorQueue();
348
349 $this->_runActionMethod('pre_update', $doPre);
350
351 foreach ($this->setfields as $field)
352 {
353 $updates[] = "$field = " . $this->_prepareFieldForSql($field);
354 }
355 $updates = implode(', ', $updates);
356
357 BSApp::$db->query("UPDATE {$this->prefix}{$this->table} SET $updates WHERE {$this->condition}");
358
359 $this->_runActionMethod('post_update', $doPost);
360 }
361
362 // ###################################################################
363 /**
364 * Deletes a record
365 *
366 * @param bool Run pre_remove()?
367 * @param bool Run post_remove()?
368 */
369 public function remove($doPre = true, $doPost = true)
370 {
371 if (!$this->condition)
372 {
373 $this->setCondition();
374 }
375
376 $this->fetch();
377
378 $this->_runActionMethod('pre_remove', $doPre);
379
380 BSApp::$db->query("DELETE FROM {$this->prefix}{$this->table} WHERE {$this->condition}");
381
382 $this->_runActionMethod('post_remove', $doPost);
383 }
384
385 // ###################################################################
386 /**
387 * Verifies that all required fields are set
388 */
389 private function _verifyRequired()
390 {
391 foreach ($this->fields as $name => $options)
392 {
393 if ($options[F_REQ] == REQ_YES)
394 {
395 if (!isset($this->values["$name"]))
396 {
397 $this->_error(new FieldException(sprintf(_('The required field "%1$s" was not set'), $name), $name));
398 }
399 }
400 else if ($options[F_REQ] == REQ_SET)
401 {
402 $this->{"set_$name"}();
403 }
404 }
405 }
406
407 // ###################################################################
408 /**
409 * Runs a pre- or post-action method for database commands
410 *
411 * @param string Action to run
412 * @param bool Actually run it?
413 */
414 private function _runActionMethod($method, $doRun)
415 {
416 if (!$doRun || !method_exists($this, $method))
417 {
418 return;
419 }
420
421 $this->$method();
422 }
423
424 // ###################################################################
425 /**
426 * Prepares a value for use in a SQL query; it encases and escapes
427 * strings and string-like values
428 *
429 * @param string Field name
430 *
431 * @return string Prepared value entry
432 */
433 private function _prepareFieldForSql($name)
434 {
435 $type = $this->fields["$name"][F_TYPE];
436
437 if ($type == TYPE_NONE || $type == TYPE_STR || $type == TYPE_STRUN)
438 {
439 return "'" . BSApp::$db->escapeString($this->values["$name"]) . "'";
440 }
441 else if ($type == TYPE_BOOL)
442 {
443 return ($this->values["$name"] == true ? "'1'" : "'0'");
444 }
445 else if ($type == TYPE_BIN)
446 {
447 return "'" . BSApp::$db->escapeBinary($this->values["$name"]) . "'";
448 }
449 else
450 {
451 return $this->values["$name"];
452 }
453 }
454
455 // ###################################################################
456 /**
457 * Verify field: not a zero value
458 */
459 protected function _verifyIsNotZero($field)
460 {
461 if ($this->values[$field] == 0)
462 {
463 $this->_error(new FieldException(sprintf(_('The field "%1$s" cannot be zero'), $field), $field));
464 return false;
465 }
466
467 return true;
468 }
469
470 // ###################################################################
471 /**
472 * Verify field: not empty
473 */
474 protected function _verifyIsNotEmpty($field)
475 {
476 if (empty($this->values[$field]))
477 {
478 $this->_error(new FieldException(sprintf(_('The field "%1$s" cannot be empty'), $field), $field));
479 return false;
480 }
481
482 return true;
483 }
484
485 // ###################################################################
486 /**
487 * Verify field: unique
488 */
489 protected function _verifyIsNotUnique($field)
490 {
491 $res = BSApp::$db->queryFirst("SELECT $field FROM {$this->prefix}{$this->table} WHERE $field = " . $this->_prepareFieldForSql($field) . (empty($this->condition) ? "" : " AND !({$this->condition})"));
492 if ($res)
493 {
494 $this->_error(new FieldException(sprintf(_('The "%1$s" field must contain a unique value'), $field), $field));
495 return false;
496 }
497
498 return true;
499 }
500 }
501
502 /**
503 * API Exception
504 *
505 * This class is an exception container that can be used to store a series
506 * of exceptions that can be thrown as one
507 *
508 * @author rsesek
509 * @copyright Copyright (c)2005 - 2008, Blue Static
510 * @package ISSO
511 *
512 */
513 class ApiException extends Exception
514 {
515 /**
516 * Array of exceptions
517 * @var array
518 */
519 private $exceptions = array();
520
521 // ###################################################################
522 /**
523 * Constructor
524 */
525 public function __construct()
526 {
527 parent::__construct(_('An error occurred while processing the API data.'));
528 }
529
530 // ###################################################################
531 /**
532 * Adds an exception to the collection
533 *
534 * @param Exception $e
535 */
536 public function addException(Exception $e)
537 {
538 $this->exceptions[] = $e;
539 }
540
541 // ###################################################################
542 /**
543 * Returns an array of all the exceptions in the collection
544 *
545 * @return array
546 */
547 public function getExceptions()
548 {
549 return $this->exceptions;
550 }
551 }
552
553 /**
554 * Field Exception
555 *
556 * This exception represents a problem with an API field
557 *
558 * @author rsesek
559 * @copyright Copyright (c)2005 - 2008, Blue Static
560 * @version $Id$
561 * @package ISSO
562 *
563 */
564 class FieldException extends Exception
565 {
566 /**
567 * The name of the erroring field
568 * @var string
569 */
570 private $field;
571
572 // ###################################################################
573 /**
574 * Constructor: create a new exception
575 */
576 public function __construct($error, $field)
577 {
578 $this->field = $field;
579 parent::__construct($error);
580 }
581
582 // ###################################################################
583 /**
584 * Returns the name of the field the exception is for
585 *
586 * @return string
587 */
588 public function getField()
589 {
590 return $this->field;
591 }
592 }
593
594 ?>