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