PlFlagset and PlDBTableEntry are exportable.
[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 PlDBNoSuchKeyException extends PlException
43 {
44 public function __construct($key, PlDBTable $table)
45 {
46 parent::__construct('Erreur lors de l\'accès à la base de données',
47 'No such key ' . $key . ' in table ' . $table->table);
48 }
49 }
50
51
52 class PlDBIncompleteEntryDescription extends PlException
53 {
54 public function __construct($field, PlDBTable $table)
55 {
56 parent::__construct('Erreur lors de l\'accès à la base de données',
57 'The field ' . $field . ' is required to describe an entry in table '
58 . $table->table);
59 }
60 }
61
62 class PlDBTableField
63 {
64 public $table;
65
66 public $name;
67 public $inPrimaryKey;
68
69 public $type;
70 public $typeLength;
71 public $typeParameters;
72
73 public $allowNull;
74 public $defaultValue;
75 public $autoIncrement;
76
77 private $validator;
78 private $formatter;
79
80 public function __construct(array $column)
81 {
82 $this->name = $column['Field'];
83 $this->typeParameters = explode(' ', str_replace(array('(', ')', ',', '\''), ' ',
84 $column['Type']));
85 $this->type = array_shift($this->typeParameters);
86 if ($this->type == 'enum' || $this->type == 'set') {
87 $this->typeParameters = new PlFlagSet(implode(',', $this->typeParameters));
88 } else if (ctype_digit($this->typeParameters[0])) {
89 $this->typeLength = intval($this->typeParameters[0]);
90 array_shift($this->typeParameters);
91 }
92 $this->allowNull = ($column['Null'] === 'YES');
93 $this->autoIncrement = (strpos($column['Extra'], 'auto_increment') !== false);
94 $this->inPrimaryKey = ($column['Key'] == 'PRI');
95
96 try {
97 $this->defaultValue = $this->format($column['Default']);
98 } catch (PlDBBadValueException $e) {
99 $this->defaultValue = null;
100 }
101 }
102
103 public function registerFormatter($class)
104 {
105 $this->formatter = $class;
106 }
107
108 public function registerValidator($class)
109 {
110 $this->validator = $class;
111 }
112
113 public function format($value, $badNullFallbackToDefault = false)
114 {
115 if (is_null($value)) {
116 if ($this->allowNull || $this->autoIncrement) {
117 return $value;
118 }
119 if ($badNullFallbackToDefault) {
120 return $this->defaultValue;
121 }
122 throw new PlDBBadValueException($value, $this, 'null not allowed');
123 }
124 if (!is_null($this->validator)) {
125 $class = $this->validator;
126 new $class($this, $value);
127 }
128 if (!is_null($this->formatter)) {
129 $class = $this->formatter;
130 $value = new $class($this, $value);
131 } else if ($this->type == 'enum') {
132 if (!$this->typeParameters->hasFlag($value)) {
133 throw new PlDBBadValueException($value, $this, 'invalid value for enum ' . $this->typeParameters->flags());
134 }
135 return $value;
136 } else if ($this->type == 'set') {
137 $value = new PlFlagSet($value);
138 foreach ($value as $flag) {
139 if (!$this->typeParameters->hasFlag($flag)) {
140 throw new PlDBBadValueException($value, $this, 'invalid flag for set ' . $this->typeParameters->flags());
141 }
142 }
143 return $value;
144 } else if (ends_with($this->type, 'int')) {
145 if (!is_int($value) && !ctype_digit($value)) {
146 throw new PlDBBadValueException($value, $this, 'value is not an integer');
147 }
148 $value = intval($value);
149 if (count($this->typeParameters) > 0 && $this->typeParameters[0] == 'unsigned') {
150 if ($value < 0) {
151 throw new PlDBBadValueException($value, $this, 'value is negative in an unsigned field');
152 }
153 }
154 /* TODO: Check bounds */
155 return $value;
156 } else if (ends_with($this->type, 'char')) {
157 if (strlen($value) > $this->typeLength) {
158 throw new PlDBBadValueException($value, $this, 'value is expected to be at most ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given');
159 }
160 return $value;
161 } else if (starts_with($this->type, 'date') || $this->type == 'timestamp') {
162 return new DateFieldFormatter($this, $value);
163 }
164 return $value;
165 }
166 }
167
168 interface PlDBTableFieldValidator
169 {
170 public function __construct(PlDBTableField $field, $value);
171 }
172
173 interface PlDBTableFieldFormatter extends PlDBTableFieldValidator, XDBFormat, PlExportable
174 {
175 }
176
177 class DateFieldFormatter implements PlDBTableFieldFormatter
178 {
179 private $datetime;
180 private $storageFormat;
181
182 public function __construct(PlDBTableField $field, $date)
183 {
184 $this->datetime = make_datetime($date);
185 if (is_null($this->datetime)) {
186 throw new PlDBBadValueException($date, $field, 'value is expected to be a date/time, ' . $date . ' given');
187 }
188 if ($field->type == 'date') {
189 $this->storageFormat = 'Y-m-d';
190 } else if ($field->type == 'datetime') {
191 $this->storageFormat = 'Y-m-d H:i:s';
192 } else {
193 $this->storageFormat = 'U';
194 }
195 }
196
197 public function format()
198 {
199 return XDB::escape($this->export());
200 }
201
202 public function date($format)
203 {
204 return $this->datetime->format($format);
205 }
206
207 public function export()
208 {
209 return $this->datetime->format($this->storageFormat);
210 }
211 }
212
213 class JSonFieldFormatter implements PlDBTableFieldFormatter, ArrayAccess
214 {
215 private $data;
216
217 public function __construct(PlDBTableField $field, $data)
218 {
219 if (strpos($field->type, 'text') === false) {
220 throw new PlDBBadValueException($data, $field, 'json formatting requires a text field');
221 }
222
223 if (is_string($data)) {
224 $this->data = json_decode($data, true);
225 } else if (is_object($data)) {
226 $this->data = json_decode(json_encode($data), true);
227 } else if (is_array($data)) {
228 $this->data = $data;
229 }
230
231 if (is_null($this->data)) {
232 throw new PlDBBadValueException($data, $field, 'cannot interpret data as json: ' . $data);
233 }
234 }
235
236 public function format()
237 {
238 return XDB::escape(json_encode($this->data));
239 }
240
241 public function export()
242 {
243 return $this->data;
244 }
245
246 public function offsetExists($offset)
247 {
248 return isset($this->data[$offset]);
249 }
250
251 public function offsetGet($offset)
252 {
253 return $this->data[$offset];
254 }
255
256 public function offsetSet($offset, $value)
257 {
258 $this->data[$offset] = $value;
259 }
260
261 public function offsetUnset($offset)
262 {
263 unset($this->data[$offset]);
264 }
265 }
266
267
268 /** This class aims at providing a simple interface to interact with a single
269 * table of a database. It is implemented as a wrapper around XDB.
270 */
271 class PlDBTable
272 {
273 const PRIMARY_KEY = 'PRIMARY';
274
275 public $table;
276
277 private $schema;
278 private $primaryKey;
279 private $uniqueKeys;
280 private $multipleKeys;
281 private $mutableFields;
282
283 public function __construct($table)
284 {
285 $this->table = $table;
286 $this->schema();
287 }
288
289 private function parseSchema(PlIterator $schema, PlIterator $keys)
290 {
291 $this->schema = array();
292 $this->primaryKey = array();
293 $this->uniqueKeys = array();
294 $this->multipleKeys = array();
295 $this->mutableFields = array();
296 while ($column = $schema->next()) {
297 $field = new PlDBTableField($column);
298 $this->schema[$field->name] = $field;
299 if (!$field->inPrimaryKey) {
300 $this->mutableFields[] = $field->name;
301 }
302 }
303 while ($column = $keys->next()) {
304 $name = $column['Key_name'];
305 $multiple = intval($column['Non_unique']) != 0;
306 $field = $column['Column_name'];
307 if ($multiple) {
308 if (!isset($this->multipleKeys[$name])) {
309 $this->multipleKeys[$name] = array();
310 }
311 $this->multipleKeys[$name][] = $field;
312 } else if ($name == self::PRIMARY_KEY) {
313 $this->primaryKey[] = $field;
314 } else {
315 if (!isset($this->uniqueKeys[$name])) {
316 $this->uniqueKeys[$name] = array();
317 }
318 $this->uniqueKeys[$name][] = $field;
319 }
320 }
321 }
322
323
324 private function schema()
325 {
326 if (!$this->schema) {
327 $schema = XDB::iterator('DESCRIBE ' . $this->table);
328 $keys = XDB::iterator('SHOW INDEX FROM ' . $this->table);
329 $this->parseSchema($schema, $keys);
330 }
331 return $this->schema;
332 }
333
334 private function field($field)
335 {
336 $schema = $this->schema();
337 if (!isset($schema[$field])) {
338 throw new PlDBNoSuchFieldException($field, $this);
339 }
340 return $schema[$field];
341 }
342
343 public function formatField($field, $value)
344 {
345 return $this->field($field)->format($value);
346 }
347
348 public function registerFieldFormatter($field, $class)
349 {
350 return $this->field($field)->registerFormatter($class);
351 }
352
353 public function registerFieldValidator($field, $class)
354 {
355 return $this->field($field)->registerValidator($class);
356 }
357
358
359 public function defaultValue($field)
360 {
361 return $this->field($field)->defaultValue;
362 }
363
364 private function hasKeyField(PlDBTableEntry $entry, array $fields)
365 {
366 foreach ($fields as $field) {
367 if (isset($entry->$field)) {
368 return true;
369 }
370 }
371 return false;
372 }
373
374 private function keyFields($keyName)
375 {
376 if ($keyName == self::PRIMARY_KEY) {
377 return $this->primaryKey;
378 } else if (isset($this->uniqueKeys[$keyName])) {
379 return $this->uniqueKeys[$keyName];
380 } else if (isset($this->multipleKeys[$keyName])) {
381 return $this->multipleKeys[$keyName];
382 }
383 throw new PlDBNoSuchKeyException($keyName, $this);
384 }
385
386 private function bestKeyFields(PlDBTableEntry $entry, $allowMultiple)
387 {
388 if ($this->hasKeyField($entry, $this->primaryKey)) {
389 return $this->primaryKey;
390 }
391 foreach ($this->uniqueKeys as $fields) {
392 if ($this->hasKeyField($entry, $fields)) {
393 return $fields;
394 }
395 }
396 if ($allowMultiple) {
397 foreach ($this->multipleKeys as $fields) {
398 if ($this->hasKeyField($entry, $fields)) {
399 return $fields;
400 }
401 }
402 }
403 return $this->primaryKey;
404 }
405
406 public function key(PlDBTableEntry $entry, array $keyFields)
407 {
408 $key = array();
409 foreach ($keyFields as $field) {
410 if (!isset($entry->$field)) {
411 throw new PlDBIncompleteEntryDescription($field, $this);
412 } else {
413 $key[] = XDB::escape($this->$field);
414 }
415 }
416 return implode('-', $key);
417 }
418
419 public function primaryKey(PlDBTableEntry $entry)
420 {
421 return $this->key($this->keyFields(self::PRIMARY_KEY));
422 }
423
424 private function buildKeyCondition(PlDBTableEntry $entry, array $keyFields, $allowIncomplete)
425 {
426 $condition = array();
427 foreach ($keyFields as $field) {
428 if (!isset($entry->$field)) {
429 if (!$allowIncomplete) {
430 throw new PlDBIncompleteEntryDescription($field, $this);
431 }
432 } else {
433 $condition[] = XDB::format($field . ' = {?}', $entry->$field);
434 }
435 }
436 return implode(' AND ', $condition);
437 }
438
439 public function fetchEntry(PlDBTableEntry $entry)
440 {
441 $result = XDB::rawFetchOneAssoc('SELECT *
442 FROM ' . $this->table . '
443 WHERE ' . $this->buildKeyCondition($entry,
444 $this->bestKeyFields($entry, false),
445 false));
446 if (!$result) {
447 return false;
448 }
449 return $entry->fillFromDBData($result);
450 }
451
452 public function iterateOnCondition(PlDBTableEntry $entry, $condition, $sortField)
453 {
454 if (empty($sortField)) {
455 $sortField = $this->primaryKey;
456 }
457 if (!is_array($sortField)) {
458 $sortField = array($sortField);
459 }
460 $sort = ' ORDER BY ' . implode(', ', $sortField);
461 $it = XDB::rawIterator('SELECT *
462 FROM ' . $this->table . '
463 WHERE ' . $condition . '
464 ' . $sort);
465 return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData'));
466 }
467
468 public function iterateOnEntry(PlDBTableEntry $entry, $sortField)
469 {
470 return $this->iterateOnCondition($entry,
471 $this->buildKeyCondition($entry,
472 $this->bestKeyFields($entry, true),
473 true),
474 $sortField);
475 }
476
477 const SAVE_INSERT_MISSING = 0x01;
478 const SAVE_UPDATE_EXISTING = 0x02;
479 const SAVE_IGNORE_DUPLICATE = 0x04;
480 public function saveEntry(PlDBTableEntry $entry, $flags)
481 {
482 $flags &= (self::SAVE_INSERT_MISSING | self::SAVE_UPDATE_EXISTING | self::SAVE_IGNORE_DUPLICATE);
483 Platal::assert($flags != 0, "Hey, the flags ($flags) here are so stupid, don't know what to do");
484 if ($flags == self::SAVE_UPDATE_EXISTING) {
485 $values = array();
486 foreach ($this->mutableFields as $field) {
487 if ($entry->hasChanged($field)) {
488 $values[] = XDB::format($field . ' = {?}', $entry->$field);
489 }
490 }
491 if (count($values) > 0) {
492 XDB::rawExecute('UPDATE ' . $this->table . '
493 SET ' . implode(', ', $values) . '
494 WHERE ' . $this->buildKeyCondition($entry,
495 $this->keyFields(self::PRIMARY_KEY),
496 false));
497 }
498 } else {
499 $values = array();
500 foreach ($this->schema as $field=>$type) {
501 if ($entry->hasChanged($field)) {
502 $values[$field] = XDB::escape($entry->$field);
503 }
504 }
505 if (count($values) > 0) {
506 $query = $this->table . ' (' . implode(', ', array_keys($values)) . ')
507 VALUES (' . implode(', ', $values) . ')';
508 if (($flags & self::SAVE_UPDATE_EXISTING)) {
509 $update = array();
510 foreach ($this->mutableFields as $field) {
511 if (isset($values[$field])) {
512 $update[] = "$field = VALUES($field)";
513 }
514 }
515 if (count($update) > 0) {
516 $query = 'INSERT ' . $query;
517 $query .= "\n ON DUPLICATE KEY UPDATE " . implode(', ', $update);
518 } else {
519 $query = 'INSERT IGNORE ' . $query;
520 }
521 } else if (($flags & self::SAVE_IGNORE_DUPLICATE)) {
522 $query = 'INSERT IGNORE ' . $query;
523 } else {
524 $query = 'INSERT ' . $query;
525 }
526 XDB::rawExecute($query);
527 $id = XDB::insertId();
528 if ($id) {
529 foreach ($this->primaryKey as $field) {
530 if ($this->schema[$field]->autoIncrement) {
531 $entry->$field = $id;
532 break;
533 }
534 }
535 }
536 }
537 }
538 }
539
540 public function deleteEntry(PlDBTableEntry $entry, $allowIncomplete)
541 {
542 XDB::rawExecute('DELETE FROM ' . $this->table . '
543 WHERE ' . $this->buildKeyCondition($entry,
544 $this->bestKeyFields($entry, $allowIncomplete),
545 $allowIncomplete));
546 }
547
548 public function exportEntry(PlDBTableEntry $entry)
549 {
550 $export = array();
551 foreach ($this->schema as $key=>$field) {
552 $value = $entry->$key;
553 if ($value instanceof PlExportable) {
554 $value = $value->export();
555 }
556 $export[$key] = $value;
557 }
558 return $export;
559 }
560
561 public static function get($name)
562 {
563 return new PlDBTable($name);
564 }
565 }
566
567 class PlDBTableEntry extends PlAbstractIterable implements PlExportable
568 {
569 private $table;
570 private $changed;
571 private $fetched = false;
572 private $autoFetch;
573
574 private $data = array();
575
576 public function __construct($table, $autoFetch = false)
577 {
578 if ($table instanceof PlDBTable) {
579 $this->table = $table;
580 } else {
581 $this->table = PlCache::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table));
582 }
583 $this->autoFetch = $autoFetch;
584 $this->changed = new PlFlagSet();
585 }
586
587 /** Register a custom formatter for a field.
588 *
589 * A formatter can be used to perform on-the-fly conversion from db storage to a user-friendly format.
590 * For example, if you have a textual field that contain json, you can use a JSonFieldFormatter on this
591 * field to perform automatic decoding when reading from the database (or when assigning the field)
592 * and automatic json_encoding when storing the object back to the db.
593 */
594 protected function registerFieldFormatter($field, $formatterClass)
595 {
596 $this->table->registerFieldFormatter($field, $formatterClass);
597 }
598
599 /** Register a custom validator for a field.
600 *
601 * A validator perform a pre-filter on the value of a field. As opposed to the formatters, it does
602 * not affects how the value is stored in the database.
603 */
604 protected function registerFieldValidator($field, $validatorClass)
605 {
606 $this->table->registerFieldValidator($field, $validatorClass);
607 }
608
609 /** This hook is called when the entry is going to be updated in the db.
610 *
611 * A typical usecase is a class that stores low-level representation of
612 * an object in db and perform a conversion between this low-level representation
613 * and a higher-level representation.
614 *
615 * @return true in case of success
616 */
617 protected function preSave()
618 {
619 return true;
620 }
621
622 /** This hook is called when the entry has been save in the database.
623 *
624 * It can be used to perform post-actions on save like storing extra data
625 * in database or sending a notification.
626 */
627 protected function postSave()
628 {
629 }
630
631 /** This hook is called when the entry is going to be deleted from the db.
632 *
633 * Default behavior is to call preSave().
634 *
635 * @return true in case of success.
636 */
637 protected function preDelete()
638 {
639 return $this->preSave();
640 }
641
642 /** This hook is called when the entry has just been fetched from the db.
643 *
644 * This is the counterpart of @ref preSave and a typical use-case is the conversion
645 * from a high-level representation of the objet to a representation suitable for
646 * storage in the database.
647 *
648 * @return true in case of success.
649 */
650 protected function postFetch()
651 {
652 return true;
653 }
654
655 public function __get($field)
656 {
657 if (isset($this->data[$field])) {
658 return $this->data[$field];
659 } else if (!$this->fetched && $this->autoFetch) {
660 $this->fetch();
661 if (isset($this->data[$field])) {
662 return $this->data[$field];
663 }
664 }
665 return $this->table->defaultValue($field);
666 }
667
668 public function __set($field, $value)
669 {
670 $this->data[$field] = $this->table->formatField($field, $value);
671 $this->changed->addFlag($field);
672 }
673
674 public function __isset($field)
675 {
676 return isset($this->data[$field]);
677 }
678
679 public function primaryKey()
680 {
681 $this->table->primaryKey($this);
682 }
683
684 public function hasChanged($field)
685 {
686 return $this->changed->hasFlag($field);
687 }
688
689 public function fillFromArray(array $data)
690 {
691 foreach ($data as $field => $value) {
692 $this->$field = $value;
693 }
694 }
695
696 public function fillFromDBData(array $data)
697 {
698 $this->fillFromArray($data);
699 $this->changed->clear();
700 return $this->postFetch();
701 }
702
703 public function copy(PlDBTableEntry $other)
704 {
705 Platal::assert($this->table == $other->table,
706 "Trying to fill an entry of table {$this->table->table} with content of {$other->table->table}.");
707 $this->changed = $other->changed;
708 $this->fetched = $other->fetched;
709 $this->data = $other->data;
710 }
711
712 public function cloneAndFillFromDBData(array $data)
713 {
714 $clone = clone $this;
715 $clone->fillFromDBData($data);
716 return $clone;
717 }
718
719 public function fetch()
720 {
721 return $this->table->fetchEntry($this);
722 }
723
724 public function iterate($sortField = null)
725 {
726 return $this->table->iterateOnEntry($this, $sortField);
727 }
728
729 public function iterateOnCondition($condition, $sortField = null)
730 {
731 return $this->table->iterateOnCondition($this, $condition, $sortField);
732 }
733
734 public function save($flags)
735 {
736 if (!$this->preSave()) {
737 return false;
738 }
739 $this->table->saveEntry($this, $flags);
740 $this->changed->clear();
741 $this->postSave();
742 return true;
743 }
744
745 public function update($insertMissing = false)
746 {
747 $flags = PlDBTable::SAVE_UPDATE_EXISTING;
748 if ($insertMissing) {
749 $flags = PlDBTable::SAVE_INSERT_MISSING;
750 }
751 return $this->save($flags);
752 }
753
754 public function insert($allowUpdate = false)
755 {
756 $flags = PlDBTable::SAVE_INSERT_MISSING;
757 if ($allowUpdate) {
758 $flags |= PlDBTable::SAVE_UPDATE_EXISTING;
759 }
760 return $this->save($flags);
761 }
762
763 public function delete()
764 {
765 if (!$this->preDelete()) {
766 return 0;
767 }
768 return $this->table->deleteEntry($this, true);
769 }
770
771 public function export()
772 {
773 return $this->table->exportEntry($this);
774 }
775 }
776
777 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
778 ?>