| 1 | <?php |
| 2 | /*************************************************************************** |
| 3 | * Copyright (C) 2003-2011 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 | if ($data instanceof PlExportable) { |
| 227 | $this->data = $data->export(); |
| 228 | } else { |
| 229 | $this->data = json_decode(json_encode($data), true); |
| 230 | } |
| 231 | } else if (is_array($data)) { |
| 232 | $this->data = $data; |
| 233 | } |
| 234 | |
| 235 | if (is_null($this->data)) { |
| 236 | throw new PlDBBadValueException($data, $field, 'cannot interpret data as json: ' . $data); |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | public function format() |
| 241 | { |
| 242 | return XDB::escape(json_encode($this->data)); |
| 243 | } |
| 244 | |
| 245 | public function export() |
| 246 | { |
| 247 | return $this->data; |
| 248 | } |
| 249 | |
| 250 | public function offsetExists($offset) |
| 251 | { |
| 252 | return isset($this->data[$offset]); |
| 253 | } |
| 254 | |
| 255 | public function offsetGet($offset) |
| 256 | { |
| 257 | return $this->data[$offset]; |
| 258 | } |
| 259 | |
| 260 | public function offsetSet($offset, $value) |
| 261 | { |
| 262 | $this->data[$offset] = $value; |
| 263 | } |
| 264 | |
| 265 | public function offsetUnset($offset) |
| 266 | { |
| 267 | unset($this->data[$offset]); |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | |
| 272 | /** This class aims at providing a simple interface to interact with a single |
| 273 | * table of a database. It is implemented as a wrapper around XDB. |
| 274 | */ |
| 275 | class PlDBTable |
| 276 | { |
| 277 | const PRIMARY_KEY = 'PRIMARY'; |
| 278 | |
| 279 | public $table; |
| 280 | |
| 281 | private $schema; |
| 282 | private $primaryKey; |
| 283 | private $uniqueKeys; |
| 284 | private $multipleKeys; |
| 285 | private $mutableFields; |
| 286 | |
| 287 | public function __construct($table) |
| 288 | { |
| 289 | $this->table = $table; |
| 290 | $this->schema(); |
| 291 | } |
| 292 | |
| 293 | private function parseSchema(PlIterator $schema, PlIterator $keys) |
| 294 | { |
| 295 | $this->schema = array(); |
| 296 | $this->primaryKey = array(); |
| 297 | $this->uniqueKeys = array(); |
| 298 | $this->multipleKeys = array(); |
| 299 | $this->mutableFields = array(); |
| 300 | while ($column = $schema->next()) { |
| 301 | $field = new PlDBTableField($column); |
| 302 | $this->schema[$field->name] = $field; |
| 303 | if (!$field->inPrimaryKey) { |
| 304 | $this->mutableFields[] = $field->name; |
| 305 | } |
| 306 | } |
| 307 | while ($column = $keys->next()) { |
| 308 | $name = $column['Key_name']; |
| 309 | $multiple = intval($column['Non_unique']) != 0; |
| 310 | $field = $column['Column_name']; |
| 311 | if ($multiple) { |
| 312 | if (!isset($this->multipleKeys[$name])) { |
| 313 | $this->multipleKeys[$name] = array(); |
| 314 | } |
| 315 | $this->multipleKeys[$name][] = $field; |
| 316 | } else if ($name == self::PRIMARY_KEY) { |
| 317 | $this->primaryKey[] = $field; |
| 318 | } else { |
| 319 | if (!isset($this->uniqueKeys[$name])) { |
| 320 | $this->uniqueKeys[$name] = array(); |
| 321 | } |
| 322 | $this->uniqueKeys[$name][] = $field; |
| 323 | } |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | |
| 328 | private function schema() |
| 329 | { |
| 330 | if (!$this->schema) { |
| 331 | $schema = XDB::iterator('DESCRIBE ' . $this->table); |
| 332 | $keys = XDB::iterator('SHOW INDEX FROM ' . $this->table); |
| 333 | $this->parseSchema($schema, $keys); |
| 334 | } |
| 335 | return $this->schema; |
| 336 | } |
| 337 | |
| 338 | private function field($field) |
| 339 | { |
| 340 | $schema = $this->schema(); |
| 341 | if (!isset($schema[$field])) { |
| 342 | throw new PlDBNoSuchFieldException($field, $this); |
| 343 | } |
| 344 | return $schema[$field]; |
| 345 | } |
| 346 | |
| 347 | public function formatField($field, $value) |
| 348 | { |
| 349 | return $this->field($field)->format($value); |
| 350 | } |
| 351 | |
| 352 | public function registerFieldFormatter($field, $class) |
| 353 | { |
| 354 | return $this->field($field)->registerFormatter($class); |
| 355 | } |
| 356 | |
| 357 | public function registerFieldValidator($field, $class) |
| 358 | { |
| 359 | return $this->field($field)->registerValidator($class); |
| 360 | } |
| 361 | |
| 362 | |
| 363 | public function defaultValue($field) |
| 364 | { |
| 365 | return $this->field($field)->defaultValue; |
| 366 | } |
| 367 | |
| 368 | private function hasKeyField(PlDBTableEntry $entry, array $fields) |
| 369 | { |
| 370 | foreach ($fields as $field) { |
| 371 | if (isset($entry->$field)) { |
| 372 | return true; |
| 373 | } |
| 374 | } |
| 375 | return false; |
| 376 | } |
| 377 | |
| 378 | private function keyFields($keyName) |
| 379 | { |
| 380 | if ($keyName == self::PRIMARY_KEY) { |
| 381 | return $this->primaryKey; |
| 382 | } else if (isset($this->uniqueKeys[$keyName])) { |
| 383 | return $this->uniqueKeys[$keyName]; |
| 384 | } else if (isset($this->multipleKeys[$keyName])) { |
| 385 | return $this->multipleKeys[$keyName]; |
| 386 | } |
| 387 | throw new PlDBNoSuchKeyException($keyName, $this); |
| 388 | } |
| 389 | |
| 390 | private function bestKeyFields(PlDBTableEntry $entry, $allowMultiple) |
| 391 | { |
| 392 | if ($this->hasKeyField($entry, $this->primaryKey)) { |
| 393 | return $this->primaryKey; |
| 394 | } |
| 395 | foreach ($this->uniqueKeys as $fields) { |
| 396 | if ($this->hasKeyField($entry, $fields)) { |
| 397 | return $fields; |
| 398 | } |
| 399 | } |
| 400 | if ($allowMultiple) { |
| 401 | foreach ($this->multipleKeys as $fields) { |
| 402 | if ($this->hasKeyField($entry, $fields)) { |
| 403 | return $fields; |
| 404 | } |
| 405 | } |
| 406 | } |
| 407 | return $this->primaryKey; |
| 408 | } |
| 409 | |
| 410 | public function key(PlDBTableEntry $entry, array $keyFields) |
| 411 | { |
| 412 | $key = array(); |
| 413 | foreach ($keyFields as $field) { |
| 414 | if (!isset($entry->$field)) { |
| 415 | throw new PlDBIncompleteEntryDescription($field, $this); |
| 416 | } else { |
| 417 | $key[] = XDB::escape($this->$field); |
| 418 | } |
| 419 | } |
| 420 | return implode('-', $key); |
| 421 | } |
| 422 | |
| 423 | public function primaryKey(PlDBTableEntry $entry) |
| 424 | { |
| 425 | return $this->key($this->keyFields(self::PRIMARY_KEY)); |
| 426 | } |
| 427 | |
| 428 | private function buildKeyCondition(PlDBTableEntry $entry, array $keyFields, $allowIncomplete) |
| 429 | { |
| 430 | $condition = array(); |
| 431 | foreach ($keyFields as $field) { |
| 432 | if (!isset($entry->$field)) { |
| 433 | if (!$allowIncomplete) { |
| 434 | throw new PlDBIncompleteEntryDescription($field, $this); |
| 435 | } |
| 436 | } else { |
| 437 | $condition[] = XDB::format($field . ' = {?}', $entry->$field); |
| 438 | } |
| 439 | } |
| 440 | return implode(' AND ', $condition); |
| 441 | } |
| 442 | |
| 443 | public function fetchEntry(PlDBTableEntry $entry) |
| 444 | { |
| 445 | $result = XDB::rawFetchOneAssoc('SELECT * |
| 446 | FROM ' . $this->table . ' |
| 447 | WHERE ' . $this->buildKeyCondition($entry, |
| 448 | $this->bestKeyFields($entry, false), |
| 449 | false)); |
| 450 | if (!$result) { |
| 451 | return false; |
| 452 | } |
| 453 | return $entry->fillFromDBData($result); |
| 454 | } |
| 455 | |
| 456 | public function iterateOnCondition(PlDBTableEntry $entry, $condition, $sortField) |
| 457 | { |
| 458 | if (empty($sortField)) { |
| 459 | $sortField = $this->primaryKey; |
| 460 | } |
| 461 | if (!is_array($sortField)) { |
| 462 | $sortField = array($sortField); |
| 463 | } |
| 464 | $sort = ' ORDER BY ' . implode(', ', $sortField); |
| 465 | $it = XDB::rawIterator('SELECT * |
| 466 | FROM ' . $this->table . ' |
| 467 | WHERE ' . $condition . ' |
| 468 | ' . $sort); |
| 469 | return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData')); |
| 470 | } |
| 471 | |
| 472 | public function iterateOnEntry(PlDBTableEntry $entry, $sortField) |
| 473 | { |
| 474 | return $this->iterateOnCondition($entry, |
| 475 | $this->buildKeyCondition($entry, |
| 476 | $this->bestKeyFields($entry, true), |
| 477 | true), |
| 478 | $sortField); |
| 479 | } |
| 480 | |
| 481 | const SAVE_INSERT_MISSING = 0x01; |
| 482 | const SAVE_UPDATE_EXISTING = 0x02; |
| 483 | const SAVE_IGNORE_DUPLICATE = 0x04; |
| 484 | public function saveEntries(array $entries, $flags) |
| 485 | { |
| 486 | $flags &= (self::SAVE_INSERT_MISSING | self::SAVE_UPDATE_EXISTING | self::SAVE_IGNORE_DUPLICATE); |
| 487 | Platal::assert($flags != 0, "Hey, the flags ($flags) here are so stupid, don't know what to do"); |
| 488 | if ($flags == self::SAVE_UPDATE_EXISTING) { |
| 489 | foreach ($entries as $entry) { |
| 490 | $values = array(); |
| 491 | foreach ($this->mutableFields as $field) { |
| 492 | if ($entry->hasChanged($field)) { |
| 493 | $values[] = XDB::format($field . ' = {?}', $entry->$field); |
| 494 | } |
| 495 | } |
| 496 | if (count($values) > 0) { |
| 497 | XDB::rawExecute('UPDATE ' . $this->table . ' |
| 498 | SET ' . implode(', ', $values) . ' |
| 499 | WHERE ' . $this->buildKeyCondition($entry, |
| 500 | $this->keyFields(self::PRIMARY_KEY), |
| 501 | false)); |
| 502 | } |
| 503 | } |
| 504 | } else { |
| 505 | $fields = new PlFlagSet(); |
| 506 | foreach ($entries as $entry) { |
| 507 | foreach ($this->schema as $field=>$type) { |
| 508 | if ($type->inPrimaryKey || $entry->hasChanged($field)) { |
| 509 | $fields->addFlag($field); |
| 510 | } |
| 511 | } |
| 512 | } |
| 513 | if (count($fields->export()) > 0) { |
| 514 | foreach ($entries as $entry) { |
| 515 | $v = array(); |
| 516 | foreach ($fields as $field) { |
| 517 | $v[$field] = XDB::escape($entry->$field); |
| 518 | } |
| 519 | $values[] = '(' . implode(', ', $v) . ')'; |
| 520 | } |
| 521 | |
| 522 | $query = $this->table . ' (' . implode(', ', $fields->export()) . ') |
| 523 | VALUES ' . implode(",\n", $values); |
| 524 | if (($flags & self::SAVE_UPDATE_EXISTING)) { |
| 525 | $update = array(); |
| 526 | foreach ($this->mutableFields as $field) { |
| 527 | if (isset($values[$field])) { |
| 528 | $update[] = "$field = VALUES($field)"; |
| 529 | } |
| 530 | } |
| 531 | if (count($update) > 0) { |
| 532 | $query = 'INSERT INTO ' . $query; |
| 533 | $query .= "\n ON DUPLICATE KEY UPDATE " . implode(', ', $update); |
| 534 | } else { |
| 535 | $query = 'INSERT IGNORE INTO ' . $query; |
| 536 | } |
| 537 | } else if (($flags & self::SAVE_IGNORE_DUPLICATE)) { |
| 538 | $query = 'INSERT IGNORE INTO ' . $query; |
| 539 | } else { |
| 540 | $query = 'INSERT INTO ' . $query; |
| 541 | } |
| 542 | XDB::rawExecute($query); |
| 543 | if (count($entries) == 1) { |
| 544 | $id = XDB::insertId(); |
| 545 | if ($id) { |
| 546 | $entry = end($entries); |
| 547 | foreach ($this->primaryKey as $field) { |
| 548 | if ($this->schema[$field]->autoIncrement) { |
| 549 | $entry->$field = $id; |
| 550 | break; |
| 551 | } |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | } |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | public function deleteEntry(PlDBTableEntry $entry, $allowIncomplete) |
| 560 | { |
| 561 | XDB::rawExecute('DELETE FROM ' . $this->table . ' |
| 562 | WHERE ' . $this->buildKeyCondition($entry, |
| 563 | $this->bestKeyFields($entry, $allowIncomplete), |
| 564 | $allowIncomplete)); |
| 565 | } |
| 566 | |
| 567 | public function exportEntry(PlDBTableEntry $entry) |
| 568 | { |
| 569 | $export = array(); |
| 570 | foreach ($this->schema as $key=>$field) { |
| 571 | $value = $entry->$key; |
| 572 | if ($value instanceof PlExportable) { |
| 573 | $value = $value->export(); |
| 574 | } |
| 575 | $export[$key] = $value; |
| 576 | } |
| 577 | return $export; |
| 578 | } |
| 579 | |
| 580 | public static function get($name) |
| 581 | { |
| 582 | return new PlDBTable($name); |
| 583 | } |
| 584 | } |
| 585 | |
| 586 | class PlDBTableEntry extends PlAbstractIterable implements PlExportable |
| 587 | { |
| 588 | private $table; |
| 589 | private $changed; |
| 590 | private $fetched = false; |
| 591 | private $autoFetch; |
| 592 | |
| 593 | private $data = array(); |
| 594 | |
| 595 | public function __construct($table, $autoFetch = false) |
| 596 | { |
| 597 | if ($table instanceof PlDBTable) { |
| 598 | $this->table = $table; |
| 599 | } else { |
| 600 | $this->table = PlCache::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table)); |
| 601 | } |
| 602 | $this->autoFetch = $autoFetch; |
| 603 | $this->changed = new PlFlagSet(); |
| 604 | } |
| 605 | |
| 606 | /** Register a custom formatter for a field. |
| 607 | * |
| 608 | * A formatter can be used to perform on-the-fly conversion from db storage to a user-friendly format. |
| 609 | * For example, if you have a textual field that contain json, you can use a JSonFieldFormatter on this |
| 610 | * field to perform automatic decoding when reading from the database (or when assigning the field) |
| 611 | * and automatic json_encoding when storing the object back to the db. |
| 612 | */ |
| 613 | protected function registerFieldFormatter($field, $formatterClass) |
| 614 | { |
| 615 | $this->table->registerFieldFormatter($field, $formatterClass); |
| 616 | } |
| 617 | |
| 618 | /** Register a custom validator for a field. |
| 619 | * |
| 620 | * A validator perform a pre-filter on the value of a field. As opposed to the formatters, it does |
| 621 | * not affects how the value is stored in the database. |
| 622 | */ |
| 623 | protected function registerFieldValidator($field, $validatorClass) |
| 624 | { |
| 625 | $this->table->registerFieldValidator($field, $validatorClass); |
| 626 | } |
| 627 | |
| 628 | /** This hook is called when the entry is going to be updated in the db. |
| 629 | * |
| 630 | * A typical usecase is a class that stores low-level representation of |
| 631 | * an object in db and perform a conversion between this low-level representation |
| 632 | * and a higher-level representation. |
| 633 | * |
| 634 | * @return true in case of success |
| 635 | */ |
| 636 | protected function preSave() |
| 637 | { |
| 638 | return true; |
| 639 | } |
| 640 | |
| 641 | /** This hook is called when the entry has been save in the database. |
| 642 | * |
| 643 | * It can be used to perform post-actions on save like storing extra data |
| 644 | * in database or sending a notification. |
| 645 | */ |
| 646 | protected function postSave() |
| 647 | { |
| 648 | } |
| 649 | |
| 650 | /** This hook is called when the entry is going to be deleted from the db. |
| 651 | * |
| 652 | * Default behavior is to call preSave(). |
| 653 | * |
| 654 | * @return true in case of success. |
| 655 | */ |
| 656 | protected function preDelete() |
| 657 | { |
| 658 | return $this->preSave(); |
| 659 | } |
| 660 | |
| 661 | /** This hook is called when the entry has just been fetched from the db. |
| 662 | * |
| 663 | * This is the counterpart of @ref preSave and a typical use-case is the conversion |
| 664 | * from a high-level representation of the objet to a representation suitable for |
| 665 | * storage in the database. |
| 666 | * |
| 667 | * @return true in case of success. |
| 668 | */ |
| 669 | protected function postFetch() |
| 670 | { |
| 671 | return true; |
| 672 | } |
| 673 | |
| 674 | public function __get($field) |
| 675 | { |
| 676 | if (isset($this->data[$field])) { |
| 677 | return $this->data[$field]; |
| 678 | } else if (!$this->fetched && $this->autoFetch) { |
| 679 | $this->fetch(); |
| 680 | if (isset($this->data[$field])) { |
| 681 | return $this->data[$field]; |
| 682 | } |
| 683 | } |
| 684 | return $this->table->defaultValue($field); |
| 685 | } |
| 686 | |
| 687 | public function __set($field, $value) |
| 688 | { |
| 689 | $this->data[$field] = $this->table->formatField($field, $value); |
| 690 | $this->changed->addFlag($field); |
| 691 | } |
| 692 | |
| 693 | public function __isset($field) |
| 694 | { |
| 695 | return isset($this->data[$field]); |
| 696 | } |
| 697 | |
| 698 | public function primaryKey() |
| 699 | { |
| 700 | $this->table->primaryKey($this); |
| 701 | } |
| 702 | |
| 703 | public function hasChanged($field) |
| 704 | { |
| 705 | return $this->changed->hasFlag($field); |
| 706 | } |
| 707 | |
| 708 | public function fillFromArray(array $data) |
| 709 | { |
| 710 | foreach ($data as $field => $value) { |
| 711 | $this->$field = $value; |
| 712 | } |
| 713 | } |
| 714 | |
| 715 | public function fillFromDBData(array $data) |
| 716 | { |
| 717 | $this->fillFromArray($data); |
| 718 | $this->changed->clear(); |
| 719 | return $this->postFetch(); |
| 720 | } |
| 721 | |
| 722 | public function copy(PlDBTableEntry $other) |
| 723 | { |
| 724 | Platal::assert($this->table == $other->table, |
| 725 | "Trying to fill an entry of table {$this->table->table} with content of {$other->table->table}."); |
| 726 | $this->changed = $other->changed; |
| 727 | $this->fetched = $other->fetched; |
| 728 | $this->data = $other->data; |
| 729 | } |
| 730 | |
| 731 | public function cloneAndFillFromDBData(array $data) |
| 732 | { |
| 733 | $clone = clone $this; |
| 734 | $clone->fillFromDBData($data); |
| 735 | return $clone; |
| 736 | } |
| 737 | |
| 738 | public function fetch() |
| 739 | { |
| 740 | return $this->table->fetchEntry($this); |
| 741 | } |
| 742 | |
| 743 | public function iterate($sortField = null) |
| 744 | { |
| 745 | return $this->table->iterateOnEntry($this, $sortField); |
| 746 | } |
| 747 | |
| 748 | public function iterateOnCondition($condition, $sortField = null) |
| 749 | { |
| 750 | return $this->table->iterateOnCondition($this, $condition, $sortField); |
| 751 | } |
| 752 | |
| 753 | public function save($flags) |
| 754 | { |
| 755 | return self::saveBatch(array($this), $flags); |
| 756 | } |
| 757 | |
| 758 | public function update($insertMissing = false) |
| 759 | { |
| 760 | $flags = PlDBTable::SAVE_UPDATE_EXISTING; |
| 761 | if ($insertMissing) { |
| 762 | $flags = PlDBTable::SAVE_INSERT_MISSING; |
| 763 | } |
| 764 | return $this->save($flags); |
| 765 | } |
| 766 | |
| 767 | public function insert($allowUpdate = false) |
| 768 | { |
| 769 | $flags = PlDBTable::SAVE_INSERT_MISSING; |
| 770 | if ($allowUpdate) { |
| 771 | $flags |= PlDBTable::SAVE_UPDATE_EXISTING; |
| 772 | } |
| 773 | return $this->save($flags); |
| 774 | } |
| 775 | |
| 776 | public function delete() |
| 777 | { |
| 778 | if (!$this->preDelete()) { |
| 779 | return 0; |
| 780 | } |
| 781 | return $this->table->deleteEntry($this, true); |
| 782 | } |
| 783 | |
| 784 | public function export() |
| 785 | { |
| 786 | return $this->table->exportEntry($this); |
| 787 | } |
| 788 | |
| 789 | protected static function saveBatch($entries, $flags) |
| 790 | { |
| 791 | $table = null; |
| 792 | foreach ($entries as $entry) { |
| 793 | if (is_null($table)) { |
| 794 | $table = $entry->table; |
| 795 | } else { |
| 796 | Platal::assert($table === $entry->table, "Cannot save batch of entries of different kinds"); |
| 797 | } |
| 798 | if (!$entry->preSave()) { |
| 799 | return false; |
| 800 | } |
| 801 | } |
| 802 | $table->saveEntries($entries, $flags); |
| 803 | foreach ($entries as $entry) { |
| 804 | $entry->changed->clear(); |
| 805 | $entry->postSave(); |
| 806 | } |
| 807 | return true; |
| 808 | } |
| 809 | |
| 810 | public static function insertBatch($entries, $allowUpdate = false) |
| 811 | { |
| 812 | $flags = PlDBTable::SAVE_INSERT_MISSING; |
| 813 | if ($allowUpdate) { |
| 814 | $flags |= PlDBTable::SAVE_UPDATE_EXISTING; |
| 815 | } |
| 816 | return self::saveBatch($entries, $flags); |
| 817 | } |
| 818 | } |
| 819 | |
| 820 | // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: |
| 821 | ?> |