PlDBTableEntry: add support for custom selection.
[platal.git] / classes / pldbtableentry.php
CommitLineData
26d00fe5
FB
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
22class 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
33class 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
71db9fda
FB
42class 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
26d00fe5
FB
52class 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
62class PlDBTableField
63{
64 public $table;
65
66 public $name;
67 public $inPrimaryKey;
26d00fe5
FB
68
69 public $type;
70 public $typeLength;
71 public $typeParameters;
72
73 public $allowNull;
74 public $defaultValue;
75 public $autoIncrement;
76
77 public function __construct(array $column)
78 {
79 $this->name = $column['Field'];
80 $this->typeParameters = explode(' ', str_replace(array('(', ')', ',', '\''), ' ',
81 $column['Type']));
82 $this->type = array_shift($this->typeParameters);
83 if ($this->type == 'enum' || $this->type == 'set') {
84 $this->typeParameters = new PlFlagSet(implode(',', $this->typeParameters));
85 } else if (ctype_digit($this->typeParameters[0])) {
86 $this->typeLength = intval($this->typeParameters[0]);
87 array_shift($this->typeParameters);
88 }
89 $this->allowNull = ($column['Null'] === 'YES');
90 $this->autoIncrement = (strpos($column['Extra'], 'auto_increment') !== false);
71db9fda 91 $this->inPrimaryKey = ($column['Key'] == 'PRI');
26d00fe5
FB
92
93 try {
94 $this->defaultValue = $this->format($column['Default']);
95 } catch (PlDBBadValueException $e) {
96 $this->defaultValue = null;
97 }
98 }
99
100 public function format($value, $badNullFallbackToDefault = false)
101 {
102 if (is_null($value)) {
103 if ($this->allowNull || $this->autoIncrement) {
104 return $value;
105 }
106 if ($badNullFallbackToDefault) {
107 return $this->defaultValue;
108 }
109 throw new PlDBBadValueException($value, $this, 'null not allowed');
110 } else if ($this->type == 'enum') {
111 if (!$this->typeParameters->hasFlag($value)) {
112 throw new PlDBBadValueException($value, $this, 'invalid value for enum ' . $this->typeParameters->flags());
113 }
114 return $value;
115 } else if ($this->type == 'set') {
116 $value = new PlFlagSet($value);
117 foreach ($value as $flag) {
118 if (!$this->typeParameters->hasFlag($flag)) {
119 throw new PlDBBadValueException($value, $this, 'invalid flag for set ' . $this->typeParameters->flags());
120 }
121 }
122 return $value;
123 } else if (ends_with($this->type, 'int')) {
124 if (!is_int($value) && !ctype_digit($value)) {
125 throw new PlDBBadValueException($value, $this, 'value is not an integer');
126 }
127 $value = intval($value);
128 if (count($this->typeParameters) > 0 && $this->typeParameters[0] == 'unsigned') {
129 if ($value < 0) {
130 throw new PlDBBadValueException($value, $this, 'value is negative in an unsigned field');
131 }
132 }
133 /* TODO: Check bounds */
134 return $value;
135 } else if ($this->type == 'varchar') {
136 if (strlen($value) > $this->typeLength) {
137 throw new PlDBBadValueException($value, $this, 'value is expected to be at most ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given');
138 }
139 return $value;
140 } else if ($this->type == 'char') {
141 if (strlen($value) != $this->typeLength) {
142 throw new PlDBBadValueException($value, $this, 'value is expected to be ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given');
143 }
144 return $value;
20030248
FB
145 } else if (starts_with($this->type, 'date') || $this->type == 'timestamp') {
146 $date = $value;
147 $value = make_datetime($value);
148 if (is_null($value)) {
149 throw new PlDBBadValueException($date, $this, 'value is expected to be a date/time, ' . $date . ' given');
150 }
151 if ($this->type == 'date') {
152 $value = new DateFormatter($value, 'Y-m-d');
153 } else if ($this->type == 'datetime') {
154 $value = new DateFormatter($value, 'Y-m-d H:i:s');
155 } else {
156 $value = new DateFormatter($value, 'U');
157 }
26d00fe5 158 }
26d00fe5
FB
159 return $value;
160 }
161}
162
20030248
FB
163class DateFormatter implements XDBFormat
164{
165 private $datetime;
166 private $storageFormat;
167
168 public function __construct(DateTime $date, $storageFormat)
169 {
170 $this->datetime = $date;
171 $this->storageFormat = $storageFormat;
172 }
173
174 public function format()
175 {
71db9fda 176 return XDB::escape($this->datetime->format($this->storageFormat));
20030248
FB
177 }
178
179 public function date($format)
180 {
181 return $this->datetime->format($format);
182 }
183}
184
26d00fe5
FB
185
186/** This class aims at providing a simple interface to interact with a single
187 * table of a database. It is implemented as a wrapper around XDB.
188 */
189class PlDBTable
190{
71db9fda
FB
191 const PRIMARY_KEY = 'PRIMARY';
192
26d00fe5
FB
193 public $table;
194
195 private $schema;
71db9fda
FB
196 private $primaryKey;
197 private $uniqueKeys;
198 private $multipleKeys;
26d00fe5
FB
199 private $mutableFields;
200
201 public function __construct($table)
202 {
203 $this->table = $table;
204 $this->schema();
205 }
206
71db9fda 207 private function parseSchema(PlIterator $schema, PlIterator $keys)
26d00fe5
FB
208 {
209 $this->schema = array();
71db9fda
FB
210 $this->primaryKey = array();
211 $this->uniqueKeys = array();
212 $this->multipleKeys = array();
26d00fe5
FB
213 $this->mutableFields = array();
214 while ($column = $schema->next()) {
215 $field = new PlDBTableField($column);
216 $this->schema[$field->name] = $field;
71db9fda 217 if (!$field->inPrimaryKey) {
26d00fe5
FB
218 $this->mutableFields[] = $field->name;
219 }
220 }
71db9fda
FB
221 while ($column = $keys->next()) {
222 $name = $column['Key_name'];
223 $multiple = intval($column['Non_unique']) != 0;
224 $field = $column['Column_name'];
225 if ($multiple) {
226 if (!isset($this->multipleKeys[$name])) {
227 $this->multipleKeys[$name] = array();
228 }
229 $this->multipleKeys[$name][] = $field;
230 } else if ($name == self::PRIMARY_KEY) {
231 $this->primaryKey[] = $field;
232 } else {
233 if (!isset($this->uniqueKeys[$name])) {
234 $this->uniqueKeys[$name] = array();
235 }
236 $this->uniqueKeys[$name][] = $field;
237 }
238 }
26d00fe5
FB
239 }
240
241
242 private function schema()
243 {
244 if (!$this->schema) {
245 $schema = XDB::iterator('DESCRIBE ' . $this->table);
71db9fda
FB
246 $keys = XDB::iterator('SHOW INDEX FROM ' . $this->table);
247 $this->parseSchema($schema, $keys);
26d00fe5
FB
248 }
249 return $this->schema;
250 }
251
252 private function field($field)
253 {
254 $schema = $this->schema();
255 if (!isset($schema[$field])) {
256 throw new PlDBNoSuchFieldException($field, $this);
257 }
258 return $schema[$field];
259 }
260
261 public function formatField($field, $value)
262 {
263 return $this->field($field)->format($value);
264 }
265
266 public function defaultValue($field)
267 {
268 return $this->field($field)->defaultValue;
269 }
270
71db9fda
FB
271 private function hasKeyField(PlDBTableEntry $entry, array $fields)
272 {
273 foreach ($fields as $field) {
274 if (isset($entry->$field)) {
275 return true;
276 }
277 }
278 return false;
279 }
280
281 private function keyFields($keyName)
282 {
283 if ($keyName == self::PRIMARY_KEY) {
284 return $this->primaryKey;
285 } else if (isset($this->uniqueKeys[$keyName])) {
286 return $this->uniqueKeys[$keyName];
287 } else if (isset($this->multipleKeys[$keyName])) {
288 return $this->multipleKeys[$keyName];
289 }
290 throw new PlDBNoSuchKeyException($keyName, $this);
291 }
292
293 private function bestKeyFields(PlDBTableEntry $entry, $allowMultiple)
294 {
295 if ($this->hasKeyField($entry, $this->primaryKey)) {
296 return $this->primaryKey;
297 }
298 foreach ($this->uniqueKeys as $fields) {
299 if ($this->hasKeyField($entry, $fields)) {
300 return $fields;
301 }
302 }
303 if ($allowMultiple) {
304 foreach ($this->multipleKeys as $fields) {
305 if ($this->hasKeyField($entry, $fields)) {
306 return $fields;
307 }
308 }
309 }
310 return $this->primaryKey;
311 }
312
313 public function key(PlDBTableEntry $entry, array $keyFields)
26d00fe5
FB
314 {
315 $key = array();
71db9fda 316 foreach ($keyFields as $field) {
26d00fe5
FB
317 if (!isset($entry->$field)) {
318 throw new PlDBIncompleteEntryDescription($field, $this);
319 } else {
320 $key[] = XDB::escape($this->$field);
321 }
322 }
323 return implode('-', $key);
324 }
325
71db9fda
FB
326 public function primaryKey(PlDBTableEntry $entry)
327 {
328 return $this->key($this->keyFields(self::PRIMARY_KEY));
329 }
330
331 private function buildKeyCondition(PlDBTableEntry $entry, array $keyFields, $allowIncomplete)
26d00fe5
FB
332 {
333 $condition = array();
71db9fda 334 foreach ($keyFields as $field) {
26d00fe5
FB
335 if (!isset($entry->$field)) {
336 if (!$allowIncomplete) {
337 throw new PlDBIncompleteEntryDescription($field, $this);
338 }
339 } else {
340 $condition[] = XDB::format($field . ' = {?}', $entry->$field);
341 }
342 }
343 return implode(' AND ', $condition);
344 }
345
346 public function fetchEntry(PlDBTableEntry $entry)
347 {
348 $result = XDB::rawFetchOneAssoc('SELECT *
349 FROM ' . $this->table . '
71db9fda
FB
350 WHERE ' . $this->buildKeyCondition($entry,
351 $this->bestKeyFields($entry, false),
352 false));
26d00fe5
FB
353 if (!$result) {
354 return false;
355 }
356 return $entry->fillFromDBData($result);
357 }
358
71db9fda 359 public function iterateOnCondition(PlDBTableEntry $entry, $condition, $sortField)
26d00fe5 360 {
71db9fda
FB
361 if (empty($sortField)) {
362 $sortField = $this->primaryKey;
20030248 363 }
71db9fda
FB
364 if (!is_array($sortField)) {
365 $sortField = array($sortField);
366 }
367 $sort = ' ORDER BY ' . implode(', ', $sortField);
26d00fe5
FB
368 $it = XDB::rawIterator('SELECT *
369 FROM ' . $this->table . '
71db9fda
FB
370 WHERE ' . $condition . '
371 ' . $sort);
26d00fe5
FB
372 return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData'));
373 }
374
71db9fda
FB
375 public function iterateOnEntry(PlDBTableEntry $entry, $sortField)
376 {
377 return $this->iterateOnCondition($entry,
378 $this->buildKeyCondition($entry,
379 $this->bestKeyFields($entry, true),
380 true),
381 $sortField);
382 }
383
20030248
FB
384 const SAVE_INSERT_MISSING = 0x01;
385 const SAVE_UPDATE_EXISTING = 0x02;
386 const SAVE_IGNORE_DUPLICATE = 0x04;
387 public function saveEntry(PlDBTableEntry $entry, $flags)
26d00fe5 388 {
20030248
FB
389 $flags &= (self::SAVE_INSERT_MISSING | self::SAVE_UPDATE_EXISTING | self::SAVE_IGNORE_DUPLICATE);
390 Platal::assert($flags != 0, "Hey, the flags ($flags) here are so stupid, don't know what to do");
391 if ($flags == self::SAVE_UPDATE_EXISTING) {
392 $values = array();
393 foreach ($this->mutableFields as $field) {
394 if ($entry->hasChanged($field)) {
395 $values[] = XDB::format($field . ' = {?}', $entry->$field);
396 }
397 }
398 if (count($values) > 0) {
399 XDB::rawExecute('UPDATE ' . $this->table . '
400 SET ' . implode(', ', $values) . '
71db9fda
FB
401 WHERE ' . $this->buildKeyCondition($entry,
402 $this->keyFields(self::PRIMARY_KEY),
403 false));
20030248
FB
404 }
405 } else {
406 $values = array();
407 foreach ($this->schema as $field=>$type) {
408 if ($entry->hasChanged($field)) {
409 $values[$field] = XDB::escape($entry->$field);
410 }
411 }
412 if (count($values) > 0) {
413 $query = $this->table . ' (' . implode(', ', array_keys($values)) . ')
414 VALUES (' . implode(', ', $values) . ')';
415 if (($flags & self::SAVE_UPDATE_EXISTING)) {
416 $update = array();
417 foreach ($this->mutableFields as $field) {
418 if (isset($values[$field])) {
419 $update[] = "$field = VALUES($field)";
420 }
421 }
422 if (count($update) > 0) {
423 $query = 'INSERT ' . $query;
424 $query .= "\n ON DUPLICATE KEY UPDATE " . implode(', ', $update);
425 } else {
426 $query = 'INSERT IGNORE ' . $query;
427 }
428 } else if (($flags & self::SAVE_IGNORE_DUPLICATE)) {
429 $query = 'INSERT IGNORE ' . $query;
430 } else {
431 $query = 'INSERT ' . $query;
432 }
433 XDB::rawExecute($query);
434 $id = XDB::insertId();
435 if ($id) {
71db9fda 436 foreach ($this->primaryKey as $field) {
20030248
FB
437 if ($this->schema[$field]->autoIncrement) {
438 $entry->$field = $id;
439 break;
440 }
441 }
442 }
26d00fe5
FB
443 }
444 }
20030248
FB
445 }
446
447 public function deleteEntry(PlDBTableEntry $entry, $allowIncomplete)
448 {
449 XDB::rawExecute('DELETE FROM ' . $this->table . '
71db9fda
FB
450 WHERE ' . $this->buildKeyCondition($entry,
451 $this->bestKeyFields($entry, $allowIncomplete),
452 $allowIncomplete));
26d00fe5
FB
453 }
454
455 public static function get($name)
456 {
26d00fe5
FB
457 return new PlDBTable($name);
458 }
459}
460
461class PlDBTableEntry extends PlAbstractIterable
462{
463 private $table;
464 private $changed;
465 private $fetched = false;
466 private $autoFetch;
467
468 private $data = array();
469
470 public function __construct($table, $autoFetch = false)
471 {
472 if ($table instanceof PlDBTable) {
473 $this->table = $table;
474 } else {
475 $this->table = PlCache::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table));
476 }
477 $this->autoFetch = $autoFetch;
478 $this->changed = new PlFlagSet();
479 }
480
481 /** This hook is called when the entry is going to be updated in the db.
482 *
483 * A typical usecase is a class that stores low-level representation of
484 * an object in db and perform a conversion between this low-level representation
485 * and a higher-level representation.
486 *
487 * @return true in case of success
488 */
489 protected function preSave()
490 {
491 return true;
492 }
493
20030248
FB
494 /** This hook is called when the entry has been save in the database.
495 *
496 * It can be used to perform post-actions on save like storing extra data
497 * in database or sending a notification.
498 */
499 protected function postSave()
500 {
501 }
502
503 /** This hook is called when the entry is going to be deleted from the db.
504 *
505 * Default behavior is to call preSave().
506 *
507 * @return true in case of success.
508 */
509 protected function preDelete()
510 {
511 return $this->preSave();
512 }
513
26d00fe5
FB
514 /** This hook is called when the entry has just been fetched from the db.
515 *
516 * This is the counterpart of @ref preSave and a typical use-case is the conversion
517 * from a high-level representation of the objet to a representation suitable for
518 * storage in the database.
519 *
520 * @return true in case of success.
521 */
522 protected function postFetch()
523 {
524 return true;
525 }
526
527 public function __get($field)
528 {
529 if (isset($this->data[$field])) {
530 return $this->data[$field];
531 } else if (!$this->fetched && $this->autoFetch) {
532 $this->fetch();
533 if (isset($this->data[$field])) {
534 return $this->data[$field];
535 }
536 }
537 return $this->table->defaultValue($field);
538 }
539
540 public function __set($field, $value)
541 {
542 $this->data[$field] = $this->table->formatField($field, $value);
543 $this->changed->addFlag($field);
544 }
545
546 public function __isset($field)
547 {
548 return isset($this->data[$field]);
549 }
550
551 public function primaryKey()
552 {
553 $this->table->primaryKey($this);
554 }
555
556 public function hasChanged($field)
557 {
558 return $this->changed->hasFlag($field);
559 }
560
561 public function fillFromArray(array $data)
562 {
563 foreach ($data as $field => $value) {
564 $this->$field = $value;
565 }
566 }
567
568 public function fillFromDBData(array $data)
569 {
570 $this->fillFromArray($data);
571 $this->changed->clear();
572 return $this->postFetch();
573 }
574
20030248
FB
575 public function copy(PlDBTableEntry $other)
576 {
577 Platal::assert($this->table == $other->table,
578 "Trying to fill an entry of table {$this->table} with content of {$other->table}.");
579 $this->changed = $other->changed;
580 $this->fetched = $other->fetched;
581 $this->data = $other->data;
582 }
583
26d00fe5
FB
584 public function cloneAndFillFromDBData(array $data)
585 {
586 $clone = clone $this;
587 $clone->fillFromDBData($data);
588 return $clone;
589 }
590
591 public function fetch()
592 {
593 return $this->table->fetchEntry($this);
594 }
595
20030248 596 public function iterate($sortField = null)
26d00fe5 597 {
20030248 598 return $this->table->iterateOnEntry($this, $sortField);
26d00fe5
FB
599 }
600
71db9fda
FB
601 public function iterateOnCondition($condition, $sortField = null)
602 {
603 return $this->table->iterateOnCondition($this, $condition, $sortField);
604 }
605
20030248 606 public function save($flags)
26d00fe5
FB
607 {
608 if (!$this->preSave()) {
609 return false;
610 }
20030248 611 $this->table->saveEntry($this, $flags);
26d00fe5 612 $this->changed->clear();
20030248 613 $this->postSave();
26d00fe5
FB
614 return true;
615 }
20030248
FB
616
617 public function update($insertMissing = false)
618 {
619 $flags = PlDBTable::SAVE_UPDATE_EXISTING;
620 if ($insertMissing) {
621 $flags = PlDBTable::SAVE_INSERT_MISSING;
622 }
623 return $this->save($flags);
624 }
625
626 public function insert($allowUpdate = false)
627 {
628 $flags = PlDBTable::SAVE_INSERT_MISSING;
629 if ($allowUpdate) {
630 $flags |= PlDBTable::SAVE_UPDATE_EXISTING;
631 }
632 return $this->save($flags);
633 }
634
635 public function delete()
636 {
637 if (!$this->preDelete()) {
638 return 0;
639 }
640 return $this->table->deleteEntry($this, true);
641 }
26d00fe5
FB
642}
643
644// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
645?>