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