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');
140 /* TODO: Support data and times */
146 /** This class aims at providing a simple interface to interact with a single
147 * table of a database. It is implemented as a wrapper around XDB.
155 private $mutableFields;
157 public function __construct($table)
159 $this->table
= $table;
163 private function parseSchema(PlIterator
$schema)
165 $this->schema
= array();
166 $this->keyFields
= array();
167 $this->mutableFields
= array();
168 while ($column = $schema->next()) {
169 $field = new PlDBTableField($column);
170 $this->schema
[$field->name
] = $field;
171 if ($field->inPrimaryKey
) {
172 $this->keyFields
[] = $field->name
;
174 $this->mutableFields
[] = $field->name
;
180 private function schema()
182 if (!$this->schema
) {
183 $schema = XDB
::iterator('DESCRIBE ' . $this->table
);
184 $this->parseSchema($schema);
186 return $this->schema
;
189 private function field($field)
191 $schema = $this->schema();
192 if (!isset($schema[$field])) {
193 throw new PlDBNoSuchFieldException($field, $this);
195 return $schema[$field];
198 public function formatField($field, $value)
200 return $this->field($field)->format($value);
203 public function defaultValue($field)
205 return $this->field($field)->defaultValue
;
208 public function primaryKey(PlDBTableEntry
$entry)
211 foreach ($this->keyFields
as $field) {
212 if (!isset($entry->$field)) {
213 throw new PlDBIncompleteEntryDescription($field, $this);
215 $key[] = XDB
::escape($this->$field);
218 return implode('-', $key);
221 private function buildKeyCondition(PlDBTableEntry
$entry, $allowIncomplete)
223 $condition = array();
224 foreach ($this->keyFields
as $field) {
225 if (!isset($entry->$field)) {
226 if (!$allowIncomplete) {
227 throw new PlDBIncompleteEntryDescription($field, $this);
230 $condition[] = XDB
::format($field . ' = {?}', $entry->$field);
233 return implode(' AND ', $condition);
236 public function fetchEntry(PlDBTableEntry
$entry)
238 $result = XDB
::rawFetchOneAssoc('SELECT *
239 FROM ' . $this->table
. '
240 WHERE ' . $this->buildKeyCondition($entry, false
));
244 return $entry->fillFromDBData($result);
247 public function iterateOnEntry(PlDBTableEntry
$entry)
249 $it = XDB
::rawIterator('SELECT *
250 FROM ' . $this->table
. '
251 WHERE ' . $this->buildKeyCondition($entry, true
));
252 return PlIteratorUtils
::map($it, array($entry, 'cloneAndFillFromDBData'));
255 public function updateEntry(PlDBTableEntry
$entry)
258 foreach ($this->mutableFields
as $field) {
259 if ($entry->hasChanged($field)) {
260 $values[] = XDB
::format($field . ' = {?}', $entry->$field);
263 if (count($values) > 0) {
264 XDB
::rawExecute('UPDATE ' . $this->table
. '
265 SET ' . implode(', ', $values) . '
266 WHERE ' . $this->buildKeyCondition($entry, false
));
270 public static function get($name)
273 return new PlDBTable($name);
277 class PlDBTableEntry
extends PlAbstractIterable
281 private $fetched = false
;
284 private $data = array();
286 public function __construct($table, $autoFetch = false
)
288 if ($table instanceof PlDBTable
) {
289 $this->table
= $table;
291 $this->table
= PlCache
::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table));
293 $this->autoFetch
= $autoFetch;
294 $this->changed
= new PlFlagSet();
297 /** This hook is called when the entry is going to be updated in the db.
299 * A typical usecase is a class that stores low-level representation of
300 * an object in db and perform a conversion between this low-level representation
301 * and a higher-level representation.
303 * @return true in case of success
305 protected function preSave()
310 /** This hook is called when the entry has just been fetched from the db.
312 * This is the counterpart of @ref preSave and a typical use-case is the conversion
313 * from a high-level representation of the objet to a representation suitable for
314 * storage in the database.
316 * @return true in case of success.
318 protected function postFetch()
323 public function __get($field)
325 if (isset($this->data
[$field])) {
326 return $this->data
[$field];
327 } else if (!$this->fetched
&& $this->autoFetch
) {
329 if (isset($this->data
[$field])) {
330 return $this->data
[$field];
333 return $this->table
->defaultValue($field);
336 public function __set($field, $value)
338 $this->data
[$field] = $this->table
->formatField($field, $value);
339 $this->changed
->addFlag($field);
342 public function __isset($field)
344 return isset($this->data
[$field]);
347 public function primaryKey()
349 $this->table
->primaryKey($this);
352 public function hasChanged($field)
354 return $this->changed
->hasFlag($field);
357 public function fillFromArray(array $data)
359 foreach ($data as $field => $value) {
360 $this->$field = $value;
364 public function fillFromDBData(array $data)
366 $this->fillFromArray($data);
367 $this->changed
->clear();
368 return $this->postFetch();
371 public function cloneAndFillFromDBData(array $data)
373 $clone = clone $this;
374 $clone->fillFromDBData($data);
378 public function fetch()
380 return $this->table
->fetchEntry($this);
383 public function iterate()
385 return $this->table
->iterateOnEntry($this);
388 public function save()
390 if (!$this->preSave()) {
393 $this->table
->updateEntry($this);
394 $this->changed
->clear();
399 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: