a432dd97f91c52ce844fe00e85fcbc45c0c816b7
[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 } else if (starts_with($this->type, 'date') || $this->type == 'timestamp') {
140 $date = $value;
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');
144 }
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');
149 } else {
150 $value = new DateFormatter($value, 'U');
151 }
152 }
153 return $value;
154 }
155 }
156
157 class DateFormatter implements XDBFormat
158 {
159 private $datetime;
160 private $storageFormat;
161
162 public function __construct(DateTime $date, $storageFormat)
163 {
164 $this->datetime = $date;
165 $this->storageFormat = $storageFormat;
166 }
167
168 public function format()
169 {
170 return $this->datetime->format($this->storageFormat);
171 }
172
173 public function date($format)
174 {
175 return $this->datetime->format($format);
176 }
177 }
178
179
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.
182 */
183 class PlDBTable
184 {
185 public $table;
186
187 private $schema;
188 private $keyFields;
189 private $mutableFields;
190
191 public function __construct($table)
192 {
193 $this->table = $table;
194 $this->schema();
195 }
196
197 private function parseSchema(PlIterator $schema)
198 {
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;
207 } else {
208 $this->mutableFields[] = $field->name;
209 }
210 }
211 }
212
213
214 private function schema()
215 {
216 if (!$this->schema) {
217 $schema = XDB::iterator('DESCRIBE ' . $this->table);
218 $this->parseSchema($schema);
219 }
220 return $this->schema;
221 }
222
223 private function field($field)
224 {
225 $schema = $this->schema();
226 if (!isset($schema[$field])) {
227 throw new PlDBNoSuchFieldException($field, $this);
228 }
229 return $schema[$field];
230 }
231
232 public function formatField($field, $value)
233 {
234 return $this->field($field)->format($value);
235 }
236
237 public function defaultValue($field)
238 {
239 return $this->field($field)->defaultValue;
240 }
241
242 public function primaryKey(PlDBTableEntry $entry)
243 {
244 $key = array();
245 foreach ($this->keyFields as $field) {
246 if (!isset($entry->$field)) {
247 throw new PlDBIncompleteEntryDescription($field, $this);
248 } else {
249 $key[] = XDB::escape($this->$field);
250 }
251 }
252 return implode('-', $key);
253 }
254
255 private function buildKeyCondition(PlDBTableEntry $entry, $allowIncomplete)
256 {
257 $condition = array();
258 foreach ($this->keyFields as $field) {
259 if (!isset($entry->$field)) {
260 if (!$allowIncomplete) {
261 throw new PlDBIncompleteEntryDescription($field, $this);
262 }
263 } else {
264 $condition[] = XDB::format($field . ' = {?}', $entry->$field);
265 }
266 }
267 return implode(' AND ', $condition);
268 }
269
270 public function fetchEntry(PlDBTableEntry $entry)
271 {
272 $result = XDB::rawFetchOneAssoc('SELECT *
273 FROM ' . $this->table . '
274 WHERE ' . $this->buildKeyCondition($entry, false));
275 if (!$result) {
276 return false;
277 }
278 return $entry->fillFromDBData($result);
279 }
280
281 public function iterateOnEntry(PlDBTableEntry $entry, $sortField)
282 {
283 $sort = '';
284 if (!empty($sortField)) {
285 if (!is_array($sortField)) {
286 $sortField = array($sortField);
287 }
288 $sort = ' ORDER BY ' . implode(', ', $sortField);
289 }
290 $it = XDB::rawIterator('SELECT *
291 FROM ' . $this->table . '
292 WHERE ' . $this->buildKeyCondition($entry, true)
293 . $sort);
294 return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData'));
295 }
296
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)
301 {
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) {
305 $values = array();
306 foreach ($this->mutableFields as $field) {
307 if ($entry->hasChanged($field)) {
308 $values[] = XDB::format($field . ' = {?}', $entry->$field);
309 }
310 }
311 if (count($values) > 0) {
312 XDB::rawExecute('UPDATE ' . $this->table . '
313 SET ' . implode(', ', $values) . '
314 WHERE ' . $this->buildKeyCondition($entry, false));
315 }
316 } else {
317 $values = array();
318 foreach ($this->schema as $field=>$type) {
319 if ($entry->hasChanged($field)) {
320 $values[$field] = XDB::escape($entry->$field);
321 }
322 }
323 if (count($values) > 0) {
324 $query = $this->table . ' (' . implode(', ', array_keys($values)) . ')
325 VALUES (' . implode(', ', $values) . ')';
326 if (($flags & self::SAVE_UPDATE_EXISTING)) {
327 $update = array();
328 foreach ($this->mutableFields as $field) {
329 if (isset($values[$field])) {
330 $update[] = "$field = VALUES($field)";
331 }
332 }
333 if (count($update) > 0) {
334 $query = 'INSERT ' . $query;
335 $query .= "\n ON DUPLICATE KEY UPDATE " . implode(', ', $update);
336 } else {
337 $query = 'INSERT IGNORE ' . $query;
338 }
339 } else if (($flags & self::SAVE_IGNORE_DUPLICATE)) {
340 $query = 'INSERT IGNORE ' . $query;
341 } else {
342 $query = 'INSERT ' . $query;
343 }
344 XDB::rawExecute($query);
345 $id = XDB::insertId();
346 if ($id) {
347 foreach ($this->keyFields as $field) {
348 if ($this->schema[$field]->autoIncrement) {
349 $entry->$field = $id;
350 break;
351 }
352 }
353 }
354 }
355 }
356 }
357
358 public function deleteEntry(PlDBTableEntry $entry, $allowIncomplete)
359 {
360 XDB::rawExecute('DELETE FROM ' . $this->table . '
361 WHERE ' . $this->buildKeyCondition($entry, $allowIncomplete));
362 }
363
364 public static function get($name)
365 {
366 return new PlDBTable($name);
367 }
368 }
369
370 class PlDBTableEntry extends PlAbstractIterable
371 {
372 private $table;
373 private $changed;
374 private $fetched = false;
375 private $autoFetch;
376
377 private $data = array();
378
379 public function __construct($table, $autoFetch = false)
380 {
381 if ($table instanceof PlDBTable) {
382 $this->table = $table;
383 } else {
384 $this->table = PlCache::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table));
385 }
386 $this->autoFetch = $autoFetch;
387 $this->changed = new PlFlagSet();
388 }
389
390 /** This hook is called when the entry is going to be updated in the db.
391 *
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.
395 *
396 * @return true in case of success
397 */
398 protected function preSave()
399 {
400 return true;
401 }
402
403 /** This hook is called when the entry has been save in the database.
404 *
405 * It can be used to perform post-actions on save like storing extra data
406 * in database or sending a notification.
407 */
408 protected function postSave()
409 {
410 }
411
412 /** This hook is called when the entry is going to be deleted from the db.
413 *
414 * Default behavior is to call preSave().
415 *
416 * @return true in case of success.
417 */
418 protected function preDelete()
419 {
420 return $this->preSave();
421 }
422
423 /** This hook is called when the entry has just been fetched from the db.
424 *
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.
428 *
429 * @return true in case of success.
430 */
431 protected function postFetch()
432 {
433 return true;
434 }
435
436 public function __get($field)
437 {
438 if (isset($this->data[$field])) {
439 return $this->data[$field];
440 } else if (!$this->fetched && $this->autoFetch) {
441 $this->fetch();
442 if (isset($this->data[$field])) {
443 return $this->data[$field];
444 }
445 }
446 return $this->table->defaultValue($field);
447 }
448
449 public function __set($field, $value)
450 {
451 $this->data[$field] = $this->table->formatField($field, $value);
452 $this->changed->addFlag($field);
453 }
454
455 public function __isset($field)
456 {
457 return isset($this->data[$field]);
458 }
459
460 public function primaryKey()
461 {
462 $this->table->primaryKey($this);
463 }
464
465 public function hasChanged($field)
466 {
467 return $this->changed->hasFlag($field);
468 }
469
470 public function fillFromArray(array $data)
471 {
472 foreach ($data as $field => $value) {
473 $this->$field = $value;
474 }
475 }
476
477 public function fillFromDBData(array $data)
478 {
479 $this->fillFromArray($data);
480 $this->changed->clear();
481 return $this->postFetch();
482 }
483
484 public function copy(PlDBTableEntry $other)
485 {
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;
491 }
492
493 public function cloneAndFillFromDBData(array $data)
494 {
495 $clone = clone $this;
496 $clone->fillFromDBData($data);
497 return $clone;
498 }
499
500 public function fetch()
501 {
502 return $this->table->fetchEntry($this);
503 }
504
505 public function iterate($sortField = null)
506 {
507 return $this->table->iterateOnEntry($this, $sortField);
508 }
509
510 public function save($flags)
511 {
512 if (!$this->preSave()) {
513 return false;
514 }
515 $this->table->saveEntry($this, $flags);
516 $this->changed->clear();
517 $this->postSave();
518 return true;
519 }
520
521 public function update($insertMissing = false)
522 {
523 $flags = PlDBTable::SAVE_UPDATE_EXISTING;
524 if ($insertMissing) {
525 $flags = PlDBTable::SAVE_INSERT_MISSING;
526 }
527 return $this->save($flags);
528 }
529
530 public function insert($allowUpdate = false)
531 {
532 $flags = PlDBTable::SAVE_INSERT_MISSING;
533 if ($allowUpdate) {
534 $flags |= PlDBTable::SAVE_UPDATE_EXISTING;
535 }
536 return $this->save($flags);
537 }
538
539 public function delete()
540 {
541 if (!$this->preDelete()) {
542 return 0;
543 }
544 return $this->table->deleteEntry($this, true);
545 }
546 }
547
548 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
549 ?>