Fix check.
[platal.git] / classes / pldbtableentry.php
1 <?php
2 /***************************************************************************
3 * Copyright (C) 2003-2010 Polytechnique.org *
4 * http://opensource.polytechnique.org/ *
5 * *
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. *
10 * *
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. *
15 * *
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 *
18 * Foundation, Inc., *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
21
22 class PlDBBadValueException extends PlException
23 {
24 public function __construct($value, PlDBTableField $field, $reason)
25 {
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 . '\'): '
29 . $reason);
30 }
31 }
32
33 class PlDBNoSuchFieldException extends PlException
34 {
35 public function __construct($field, PlDBTable $table)
36 {
37 parent::__construct('Erreur lors de l\'accès à la base de données',
38 'No such field ' . $field . ' in table ' . $table->table);
39 }
40 }
41
42 class PlDBIncompleteEntryDescription extends PlException
43 {
44 public function __construct($field, PlDBTable $table)
45 {
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 '
48 . $table->table);
49 }
50 }
51
52 class PlDBTableField
53 {
54 public $table;
55
56 public $name;
57 public $inPrimaryKey;
58 public $inUniqueKey;
59 public $inKey;
60
61 public $type;
62 public $typeLength;
63 public $typeParameters;
64
65 public $allowNull;
66 public $defaultValue;
67 public $autoIncrement;
68
69 public function __construct(array $column)
70 {
71 $this->name = $column['Field'];
72 $this->typeParameters = explode(' ', str_replace(array('(', ')', ',', '\''), ' ',
73 $column['Type']));
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);
80 }
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');
86
87 try {
88 $this->defaultValue = $this->format($column['Default']);
89 } catch (PlDBBadValueException $e) {
90 $this->defaultValue = null;
91 }
92 }
93
94 public function format($value, $badNullFallbackToDefault = false)
95 {
96 if (is_null($value)) {
97 if ($this->allowNull || $this->autoIncrement) {
98 return $value;
99 }
100 if ($badNullFallbackToDefault) {
101 return $this->defaultValue;
102 }
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());
107 }
108 return $value;
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());
114 }
115 }
116 return $value;
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');
120 }
121 $value = intval($value);
122 if (count($this->typeParameters) > 0 && $this->typeParameters[0] == 'unsigned') {
123 if ($value < 0) {
124 throw new PlDBBadValueException($value, $this, 'value is negative in an unsigned field');
125 }
126 }
127 /* TODO: Check bounds */
128 return $value;
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');
132 }
133 return $value;
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');
137 }
138 return $value;
139 }
140 /* TODO: Support data and times */
141 return $value;
142 }
143 }
144
145
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.
148 */
149 class PlDBTable
150 {
151 public $table;
152
153 private $schema;
154 private $keyFields;
155 private $mutableFields;
156
157 public function __construct($table)
158 {
159 $this->table = $table;
160 $this->schema();
161 }
162
163 private function parseSchema(PlIterator $schema)
164 {
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;
173 } else {
174 $this->mutableFields[] = $field->name;
175 }
176 }
177 }
178
179
180 private function schema()
181 {
182 if (!$this->schema) {
183 $schema = XDB::iterator('DESCRIBE ' . $this->table);
184 $this->parseSchema($schema);
185 }
186 return $this->schema;
187 }
188
189 private function field($field)
190 {
191 $schema = $this->schema();
192 if (!isset($schema[$field])) {
193 throw new PlDBNoSuchFieldException($field, $this);
194 }
195 return $schema[$field];
196 }
197
198 public function formatField($field, $value)
199 {
200 return $this->field($field)->format($value);
201 }
202
203 public function defaultValue($field)
204 {
205 return $this->field($field)->defaultValue;
206 }
207
208 public function primaryKey(PlDBTableEntry $entry)
209 {
210 $key = array();
211 foreach ($this->keyFields as $field) {
212 if (!isset($entry->$field)) {
213 throw new PlDBIncompleteEntryDescription($field, $this);
214 } else {
215 $key[] = XDB::escape($this->$field);
216 }
217 }
218 return implode('-', $key);
219 }
220
221 private function buildKeyCondition(PlDBTableEntry $entry, $allowIncomplete)
222 {
223 $condition = array();
224 foreach ($this->keyFields as $field) {
225 if (!isset($entry->$field)) {
226 if (!$allowIncomplete) {
227 throw new PlDBIncompleteEntryDescription($field, $this);
228 }
229 } else {
230 $condition[] = XDB::format($field . ' = {?}', $entry->$field);
231 }
232 }
233 return implode(' AND ', $condition);
234 }
235
236 public function fetchEntry(PlDBTableEntry $entry)
237 {
238 $result = XDB::rawFetchOneAssoc('SELECT *
239 FROM ' . $this->table . '
240 WHERE ' . $this->buildKeyCondition($entry, false));
241 if (!$result) {
242 return false;
243 }
244 return $entry->fillFromDBData($result);
245 }
246
247 public function iterateOnEntry(PlDBTableEntry $entry)
248 {
249 $it = XDB::rawIterator('SELECT *
250 FROM ' . $this->table . '
251 WHERE ' . $this->buildKeyCondition($entry, true));
252 return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData'));
253 }
254
255 public function updateEntry(PlDBTableEntry $entry)
256 {
257 $values = array();
258 foreach ($this->mutableFields as $field) {
259 if ($entry->hasChanged($field)) {
260 $values[] = XDB::format($field . ' = {?}', $entry->$field);
261 }
262 }
263 if (count($values) > 0) {
264 XDB::rawExecute('UPDATE ' . $this->table . '
265 SET ' . implode(', ', $values) . '
266 WHERE ' . $this->buildKeyCondition($entry, false));
267 }
268 }
269
270 public static function get($name)
271 {
272 var_dump('blah');
273 return new PlDBTable($name);
274 }
275 }
276
277 class PlDBTableEntry extends PlAbstractIterable
278 {
279 private $table;
280 private $changed;
281 private $fetched = false;
282 private $autoFetch;
283
284 private $data = array();
285
286 public function __construct($table, $autoFetch = false)
287 {
288 if ($table instanceof PlDBTable) {
289 $this->table = $table;
290 } else {
291 $this->table = PlCache::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table));
292 }
293 $this->autoFetch = $autoFetch;
294 $this->changed = new PlFlagSet();
295 }
296
297 /** This hook is called when the entry is going to be updated in the db.
298 *
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.
302 *
303 * @return true in case of success
304 */
305 protected function preSave()
306 {
307 return true;
308 }
309
310 /** This hook is called when the entry has just been fetched from the db.
311 *
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.
315 *
316 * @return true in case of success.
317 */
318 protected function postFetch()
319 {
320 return true;
321 }
322
323 public function __get($field)
324 {
325 if (isset($this->data[$field])) {
326 return $this->data[$field];
327 } else if (!$this->fetched && $this->autoFetch) {
328 $this->fetch();
329 if (isset($this->data[$field])) {
330 return $this->data[$field];
331 }
332 }
333 return $this->table->defaultValue($field);
334 }
335
336 public function __set($field, $value)
337 {
338 $this->data[$field] = $this->table->formatField($field, $value);
339 $this->changed->addFlag($field);
340 }
341
342 public function __isset($field)
343 {
344 return isset($this->data[$field]);
345 }
346
347 public function primaryKey()
348 {
349 $this->table->primaryKey($this);
350 }
351
352 public function hasChanged($field)
353 {
354 return $this->changed->hasFlag($field);
355 }
356
357 public function fillFromArray(array $data)
358 {
359 foreach ($data as $field => $value) {
360 $this->$field = $value;
361 }
362 }
363
364 public function fillFromDBData(array $data)
365 {
366 $this->fillFromArray($data);
367 $this->changed->clear();
368 return $this->postFetch();
369 }
370
371 public function cloneAndFillFromDBData(array $data)
372 {
373 $clone = clone $this;
374 $clone->fillFromDBData($data);
375 return $clone;
376 }
377
378 public function fetch()
379 {
380 return $this->table->fetchEntry($this);
381 }
382
383 public function iterate()
384 {
385 return $this->table->iterateOnEntry($this);
386 }
387
388 public function save()
389 {
390 if (!$this->preSave()) {
391 return false;
392 }
393 $this->table->updateEntry($this);
394 $this->changed->clear();
395 return true;
396 }
397 }
398
399 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
400 ?>