2 /***************************************************************************
3 * Copyright (C) 2003-2010 Polytechnique.org *
4 * http://opensource.polytechnique.org/ *
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program; if not, write to the Free Software *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
22 class PlDBBadValueException
extends PlException
24 public function __construct($value, PlDBTableField
$field, $reason)
26 parent
::__construct('Erreur lors de l\'accès à la base de données',
27 'Illegal value '. (is_null($value) ?
'(null)' : '(\'' . $value . '\')')
28 . ' for field (\'' . $field->table
->table
. '.' . $field->name
. '\'): '
33 class PlDBNoSuchFieldException
extends PlException
35 public function __construct($field, PlDBTable
$table)
37 parent
::__construct('Erreur lors de l\'accès à la base de données',
38 'No such field ' . $field . ' in table ' . $table->table
);
42 class PlDBIncompleteEntryDescription
extends PlException
44 public function __construct($field, PlDBTable
$table)
46 parent
::__construct('Erreur lors de l\'accès à la base de données',
47 'The field ' . $field . ' is required to describe an entry in table '
63 public $typeParameters;
67 public $autoIncrement;
69 public function __construct(array $column)
71 $this->name
= $column['Field'];
72 $this->typeParameters
= explode(' ', str_replace(array('(', ')', ',', '\''), ' ',
74 $this->type
= array_shift($this->typeParameters
);
75 if ($this->type
== 'enum' ||
$this->type
== 'set') {
76 $this->typeParameters
= new PlFlagSet(implode(',', $this->typeParameters
));
77 } else if (ctype_digit($this->typeParameters
[0])) {
78 $this->typeLength
= intval($this->typeParameters
[0]);
79 array_shift($this->typeParameters
);
81 $this->allowNull
= ($column['Null'] === 'YES');
82 $this->autoIncrement
= (strpos($column['Extra'], 'auto_increment') !== false
);
83 $this->inPrimaryKey
= ($column['Key'] === 'PRI');
84 $this->inUniqueKey
= $this->inPrimaryKey ||
($column['Key'] === 'UNI');
85 $this->inKey
= $this->inUniqueKey ||
($column['Key'] === 'MUL');
88 $this->defaultValue
= $this->format($column['Default']);
89 } catch (PlDBBadValueException
$e) {
90 $this->defaultValue
= null
;
94 public function format($value, $badNullFallbackToDefault = false
)
96 if (is_null($value)) {
97 if ($this->allowNull ||
$this->autoIncrement
) {
100 if ($badNullFallbackToDefault) {
101 return $this->defaultValue
;
103 throw new PlDBBadValueException($value, $this, 'null not allowed');
104 } else if ($this->type
== 'enum') {
105 if (!$this->typeParameters
->hasFlag($value)) {
106 throw new PlDBBadValueException($value, $this, 'invalid value for enum ' . $this->typeParameters
->flags());
109 } else if ($this->type
== 'set') {
110 $value = new PlFlagSet($value);
111 foreach ($value as $flag) {
112 if (!$this->typeParameters
->hasFlag($flag)) {
113 throw new PlDBBadValueException($value, $this, 'invalid flag for set ' . $this->typeParameters
->flags());
117 } else if (ends_with($this->type
, 'int')) {
118 if (!is_int($value) && !ctype_digit($value)) {
119 throw new PlDBBadValueException($value, $this, 'value is not an integer');
121 $value = intval($value);
122 if (count($this->typeParameters
) > 0 && $this->typeParameters
[0] == 'unsigned') {
124 throw new PlDBBadValueException($value, $this, 'value is negative in an unsigned field');
127 /* TODO: Check bounds */
129 } else if ($this->type
== 'varchar') {
130 if (strlen($value) > $this->typeLength
) {
131 throw new PlDBBadValueException($value, $this, 'value is expected to be at most ' . $this->typeLength
. ' characters long, ' . strlen($value) . ' given');
134 } else if ($this->type
== 'char') {
135 if (strlen($value) != $this->typeLength
) {
136 throw new PlDBBadValueException($value, $this, 'value is expected to be ' . $this->typeLength
. ' characters long, ' . strlen($value) . ' given');
139 } else if (starts_with($this->type
, 'date') ||
$this->type
== 'timestamp') {
141 $value = make_datetime($value);
142 if (is_null($value)) {
143 throw new PlDBBadValueException($date, $this, 'value is expected to be a date/time, ' . $date . ' given');
145 if ($this->type
== 'date') {
146 $value = new DateFormatter($value, 'Y-m-d');
147 } else if ($this->type
== 'datetime') {
148 $value = new DateFormatter($value, 'Y-m-d H:i:s');
150 $value = new DateFormatter($value, 'U');
157 class DateFormatter
implements XDBFormat
160 private $storageFormat;
162 public function __construct(DateTime
$date, $storageFormat)
164 $this->datetime
= $date;
165 $this->storageFormat
= $storageFormat;
168 public function format()
170 return $this->datetime
->format($this->storageFormat
);
173 public function date($format)
175 return $this->datetime
->format($format);
180 /** This class aims at providing a simple interface to interact with a single
181 * table of a database. It is implemented as a wrapper around XDB.
189 private $mutableFields;
191 public function __construct($table)
193 $this->table
= $table;
197 private function parseSchema(PlIterator
$schema)
199 $this->schema
= array();
200 $this->keyFields
= array();
201 $this->mutableFields
= array();
202 while ($column = $schema->next()) {
203 $field = new PlDBTableField($column);
204 $this->schema
[$field->name
] = $field;
205 if ($field->inPrimaryKey
) {
206 $this->keyFields
[] = $field->name
;
208 $this->mutableFields
[] = $field->name
;
214 private function schema()
216 if (!$this->schema
) {
217 $schema = XDB
::iterator('DESCRIBE ' . $this->table
);
218 $this->parseSchema($schema);
220 return $this->schema
;
223 private function field($field)
225 $schema = $this->schema();
226 if (!isset($schema[$field])) {
227 throw new PlDBNoSuchFieldException($field, $this);
229 return $schema[$field];
232 public function formatField($field, $value)
234 return $this->field($field)->format($value);
237 public function defaultValue($field)
239 return $this->field($field)->defaultValue
;
242 public function primaryKey(PlDBTableEntry
$entry)
245 foreach ($this->keyFields
as $field) {
246 if (!isset($entry->$field)) {
247 throw new PlDBIncompleteEntryDescription($field, $this);
249 $key[] = XDB
::escape($this->$field);
252 return implode('-', $key);
255 private function buildKeyCondition(PlDBTableEntry
$entry, $allowIncomplete)
257 $condition = array();
258 foreach ($this->keyFields
as $field) {
259 if (!isset($entry->$field)) {
260 if (!$allowIncomplete) {
261 throw new PlDBIncompleteEntryDescription($field, $this);
264 $condition[] = XDB
::format($field . ' = {?}', $entry->$field);
267 return implode(' AND ', $condition);
270 public function fetchEntry(PlDBTableEntry
$entry)
272 $result = XDB
::rawFetchOneAssoc('SELECT *
273 FROM ' . $this->table
. '
274 WHERE ' . $this->buildKeyCondition($entry, false
));
278 return $entry->fillFromDBData($result);
281 public function iterateOnEntry(PlDBTableEntry
$entry, $sortField)
284 if (!empty($sortField)) {
285 if (!is_array($sortField)) {
286 $sortField = array($sortField);
288 $sort = ' ORDER BY ' . implode(', ', $sortField);
290 $it = XDB
::rawIterator('SELECT *
291 FROM ' . $this->table
. '
292 WHERE ' . $this->buildKeyCondition($entry, true
)
294 return PlIteratorUtils
::map($it, array($entry, 'cloneAndFillFromDBData'));
297 const SAVE_INSERT_MISSING
= 0x01;
298 const SAVE_UPDATE_EXISTING
= 0x02;
299 const SAVE_IGNORE_DUPLICATE
= 0x04;
300 public function saveEntry(PlDBTableEntry
$entry, $flags)
302 $flags &= (self
::SAVE_INSERT_MISSING | self
::SAVE_UPDATE_EXISTING | self
::SAVE_IGNORE_DUPLICATE
);
303 Platal
::assert($flags != 0, "Hey, the flags ($flags) here are so stupid, don't know what to do");
304 if ($flags == self
::SAVE_UPDATE_EXISTING
) {
306 foreach ($this->mutableFields
as $field) {
307 if ($entry->hasChanged($field)) {
308 $values[] = XDB
::format($field . ' = {?}', $entry->$field);
311 if (count($values) > 0) {
312 XDB
::rawExecute('UPDATE ' . $this->table
. '
313 SET ' . implode(', ', $values) . '
314 WHERE ' . $this->buildKeyCondition($entry, false
));
318 foreach ($this->schema
as $field=>$type) {
319 if ($entry->hasChanged($field)) {
320 $values[$field] = XDB
::escape($entry->$field);
323 if (count($values) > 0) {
324 $query = $this->table
. ' (' . implode(', ', array_keys($values)) . ')
325 VALUES (' . implode(', ', $values) . ')';
326 if (($flags & self
::SAVE_UPDATE_EXISTING
)) {
328 foreach ($this->mutableFields
as $field) {
329 if (isset($values[$field])) {
330 $update[] = "$field = VALUES($field)";
333 if (count($update) > 0) {
334 $query = 'INSERT ' . $query;
335 $query .= "\n ON DUPLICATE KEY UPDATE " . implode(', ', $update);
337 $query = 'INSERT IGNORE ' . $query;
339 } else if (($flags & self
::SAVE_IGNORE_DUPLICATE
)) {
340 $query = 'INSERT IGNORE ' . $query;
342 $query = 'INSERT ' . $query;
344 XDB
::rawExecute($query);
345 $id = XDB
::insertId();
347 foreach ($this->keyFields
as $field) {
348 if ($this->schema
[$field]->autoIncrement
) {
349 $entry->$field = $id;
358 public function deleteEntry(PlDBTableEntry
$entry, $allowIncomplete)
360 XDB
::rawExecute('DELETE FROM ' . $this->table
. '
361 WHERE ' . $this->buildKeyCondition($entry, $allowIncomplete));
364 public static function get($name)
366 return new PlDBTable($name);
370 class PlDBTableEntry
extends PlAbstractIterable
374 private $fetched = false
;
377 private $data = array();
379 public function __construct($table, $autoFetch = false
)
381 if ($table instanceof PlDBTable
) {
382 $this->table
= $table;
384 $this->table
= PlCache
::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table));
386 $this->autoFetch
= $autoFetch;
387 $this->changed
= new PlFlagSet();
390 /** This hook is called when the entry is going to be updated in the db.
392 * A typical usecase is a class that stores low-level representation of
393 * an object in db and perform a conversion between this low-level representation
394 * and a higher-level representation.
396 * @return true in case of success
398 protected function preSave()
403 /** This hook is called when the entry has been save in the database.
405 * It can be used to perform post-actions on save like storing extra data
406 * in database or sending a notification.
408 protected function postSave()
412 /** This hook is called when the entry is going to be deleted from the db.
414 * Default behavior is to call preSave().
416 * @return true in case of success.
418 protected function preDelete()
420 return $this->preSave();
423 /** This hook is called when the entry has just been fetched from the db.
425 * This is the counterpart of @ref preSave and a typical use-case is the conversion
426 * from a high-level representation of the objet to a representation suitable for
427 * storage in the database.
429 * @return true in case of success.
431 protected function postFetch()
436 public function __get($field)
438 if (isset($this->data
[$field])) {
439 return $this->data
[$field];
440 } else if (!$this->fetched
&& $this->autoFetch
) {
442 if (isset($this->data
[$field])) {
443 return $this->data
[$field];
446 return $this->table
->defaultValue($field);
449 public function __set($field, $value)
451 $this->data
[$field] = $this->table
->formatField($field, $value);
452 $this->changed
->addFlag($field);
455 public function __isset($field)
457 return isset($this->data
[$field]);
460 public function primaryKey()
462 $this->table
->primaryKey($this);
465 public function hasChanged($field)
467 return $this->changed
->hasFlag($field);
470 public function fillFromArray(array $data)
472 foreach ($data as $field => $value) {
473 $this->$field = $value;
477 public function fillFromDBData(array $data)
479 $this->fillFromArray($data);
480 $this->changed
->clear();
481 return $this->postFetch();
484 public function copy(PlDBTableEntry
$other)
486 Platal
::assert($this->table
== $other->table
,
487 "Trying to fill an entry of table {$this->table} with content of {$other->table}.");
488 $this->changed
= $other->changed
;
489 $this->fetched
= $other->fetched
;
490 $this->data
= $other->data
;
493 public function cloneAndFillFromDBData(array $data)
495 $clone = clone $this;
496 $clone->fillFromDBData($data);
500 public function fetch()
502 return $this->table
->fetchEntry($this);
505 public function iterate($sortField = null
)
507 return $this->table
->iterateOnEntry($this, $sortField);
510 public function save($flags)
512 if (!$this->preSave()) {
515 $this->table
->saveEntry($this, $flags);
516 $this->changed
->clear();
521 public function update($insertMissing = false
)
523 $flags = PlDBTable
::SAVE_UPDATE_EXISTING
;
524 if ($insertMissing) {
525 $flags = PlDBTable
::SAVE_INSERT_MISSING
;
527 return $this->save($flags);
530 public function insert($allowUpdate = false
)
532 $flags = PlDBTable
::SAVE_INSERT_MISSING
;
534 $flags |
= PlDBTable
::SAVE_UPDATE_EXISTING
;
536 return $this->save($flags);
539 public function delete()
541 if (!$this->preDelete()) {
544 return $this->table
->deleteEntry($this, true
);
548 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: