X-Git-Url: http://git.polytechnique.org/?a=blobdiff_plain;ds=sidebyside;f=classes%2Fpldbtableentry.php;h=1ab5d4cf927692bd5102b474c4f5bc991bdc6d97;hb=b967ef46141dc9499c5654cbf010dca08cf8f945;hp=2e1bc1141b0454dbbfcbaa4efce11334c1a48654;hpb=26d00fe51bd4399ec2f9dff4a797daf2fc23961a;p=platal.git diff --git a/classes/pldbtableentry.php b/classes/pldbtableentry.php index 2e1bc11..1ab5d4c 100644 --- a/classes/pldbtableentry.php +++ b/classes/pldbtableentry.php @@ -1,6 +1,6 @@ table); + } +} + + class PlDBIncompleteEntryDescription extends PlException { public function __construct($field, PlDBTable $table) @@ -55,8 +65,6 @@ class PlDBTableField public $name; public $inPrimaryKey; - public $inUniqueKey; - public $inKey; public $type; public $typeLength; @@ -66,6 +74,9 @@ class PlDBTableField public $defaultValue; public $autoIncrement; + private $validator; + private $formatter; + public function __construct(array $column) { $this->name = $column['Field']; @@ -80,9 +91,7 @@ class PlDBTableField } $this->allowNull = ($column['Null'] === 'YES'); $this->autoIncrement = (strpos($column['Extra'], 'auto_increment') !== false); - $this->inPrimaryKey = ($column['Key'] === 'PRI'); - $this->inUniqueKey = $this->inPrimaryKey || ($column['Key'] === 'UNI'); - $this->inKey = $this->inUniqueKey || ($column['Key'] === 'MUL'); + $this->inPrimaryKey = ($column['Key'] == 'PRI'); try { $this->defaultValue = $this->format($column['Default']); @@ -91,6 +100,16 @@ class PlDBTableField } } + public function registerFormatter($class) + { + $this->formatter = $class; + } + + public function registerValidator($class) + { + $this->validator = $class; + } + public function format($value, $badNullFallbackToDefault = false) { if (is_null($value)) { @@ -101,6 +120,14 @@ class PlDBTableField return $this->defaultValue; } throw new PlDBBadValueException($value, $this, 'null not allowed'); + } + if (!is_null($this->validator)) { + $class = $this->validator; + new $class($this, $value); + } + if (!is_null($this->formatter)) { + $class = $this->formatter; + $value = new $class($this, $value); } else if ($this->type == 'enum') { if (!$this->typeParameters->hasFlag($value)) { throw new PlDBBadValueException($value, $this, 'invalid value for enum ' . $this->typeParameters->flags()); @@ -126,32 +153,135 @@ class PlDBTableField } /* TODO: Check bounds */ return $value; - } else if ($this->type == 'varchar') { + } else if (ends_with($this->type, 'char')) { if (strlen($value) > $this->typeLength) { throw new PlDBBadValueException($value, $this, 'value is expected to be at most ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given'); } return $value; - } else if ($this->type == 'char') { - if (strlen($value) != $this->typeLength) { - throw new PlDBBadValueException($value, $this, 'value is expected to be ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given'); - } - return $value; + } else if (starts_with($this->type, 'date') || $this->type == 'timestamp') { + return new DateFieldFormatter($this, $value); } - /* TODO: Support data and times */ return $value; } } +interface PlDBTableFieldValidator +{ + public function __construct(PlDBTableField $field, $value); +} + +interface PlDBTableFieldFormatter extends PlDBTableFieldValidator, XDBFormat, PlExportable +{ +} + +class DateFieldFormatter implements PlDBTableFieldFormatter +{ + private $datetime; + private $storageFormat; + + public function __construct(PlDBTableField $field, $date) + { + $this->datetime = make_datetime($date); + if (is_null($this->datetime)) { + throw new PlDBBadValueException($date, $field, 'value is expected to be a date/time, ' . $date . ' given'); + } + if ($field->type == 'date') { + $this->storageFormat = 'Y-m-d'; + } else if ($field->type == 'datetime') { + $this->storageFormat = 'Y-m-d H:i:s'; + } else { + $this->storageFormat = 'U'; + } + } + + public function format() + { + return XDB::escape($this->export()); + } + + public function date($format) + { + return $this->datetime->format($format); + } + + public function export() + { + return $this->datetime->format($this->storageFormat); + } +} + +class JSonFieldFormatter implements PlDBTableFieldFormatter, ArrayAccess +{ + private $data; + + public function __construct(PlDBTableField $field, $data) + { + if (strpos($field->type, 'text') === false) { + throw new PlDBBadValueException($data, $field, 'json formatting requires a text field'); + } + + if (is_string($data)) { + $this->data = json_decode($data, true); + } else if (is_object($data)) { + if ($data instanceof PlExportable) { + $this->data = $data->export(); + } else { + $this->data = json_decode(json_encode($data), true); + } + } else if (is_array($data)) { + $this->data = $data; + } + + if (is_null($this->data)) { + throw new PlDBBadValueException($data, $field, 'cannot interpret data as json: ' . $data); + } + } + + public function format() + { + return XDB::escape(json_encode($this->data)); + } + + public function export() + { + return $this->data; + } + + public function offsetExists($offset) + { + return isset($this->data[$offset]); + } + + public function offsetGet($offset) + { + return $this->data[$offset]; + } + + public function offsetSet($offset, $value) + { + $this->data[$offset] = $value; + } + + public function offsetUnset($offset) + { + unset($this->data[$offset]); + } +} + /** This class aims at providing a simple interface to interact with a single * table of a database. It is implemented as a wrapper around XDB. */ class PlDBTable { + const PRIMARY_KEY = 'PRIMARY'; + public $table; private $schema; - private $keyFields; + private $primaryKey; + private $uniqueKeys; + private $multipleKeys; private $mutableFields; public function __construct($table) @@ -160,20 +290,38 @@ class PlDBTable $this->schema(); } - private function parseSchema(PlIterator $schema) + private function parseSchema(PlIterator $schema, PlIterator $keys) { $this->schema = array(); - $this->keyFields = array(); + $this->primaryKey = array(); + $this->uniqueKeys = array(); + $this->multipleKeys = array(); $this->mutableFields = array(); while ($column = $schema->next()) { $field = new PlDBTableField($column); $this->schema[$field->name] = $field; - if ($field->inPrimaryKey) { - $this->keyFields[] = $field->name; - } else { + if (!$field->inPrimaryKey) { $this->mutableFields[] = $field->name; } } + while ($column = $keys->next()) { + $name = $column['Key_name']; + $multiple = intval($column['Non_unique']) != 0; + $field = $column['Column_name']; + if ($multiple) { + if (!isset($this->multipleKeys[$name])) { + $this->multipleKeys[$name] = array(); + } + $this->multipleKeys[$name][] = $field; + } else if ($name == self::PRIMARY_KEY) { + $this->primaryKey[] = $field; + } else { + if (!isset($this->uniqueKeys[$name])) { + $this->uniqueKeys[$name] = array(); + } + $this->uniqueKeys[$name][] = $field; + } + } } @@ -181,7 +329,8 @@ class PlDBTable { if (!$this->schema) { $schema = XDB::iterator('DESCRIBE ' . $this->table); - $this->parseSchema($schema); + $keys = XDB::iterator('SHOW INDEX FROM ' . $this->table); + $this->parseSchema($schema, $keys); } return $this->schema; } @@ -200,15 +349,68 @@ class PlDBTable return $this->field($field)->format($value); } + public function registerFieldFormatter($field, $class) + { + return $this->field($field)->registerFormatter($class); + } + + public function registerFieldValidator($field, $class) + { + return $this->field($field)->registerValidator($class); + } + + public function defaultValue($field) { return $this->field($field)->defaultValue; } - public function primaryKey(PlDBTableEntry $entry) + private function hasKeyField(PlDBTableEntry $entry, array $fields) + { + foreach ($fields as $field) { + if (isset($entry->$field)) { + return true; + } + } + return false; + } + + private function keyFields($keyName) + { + if ($keyName == self::PRIMARY_KEY) { + return $this->primaryKey; + } else if (isset($this->uniqueKeys[$keyName])) { + return $this->uniqueKeys[$keyName]; + } else if (isset($this->multipleKeys[$keyName])) { + return $this->multipleKeys[$keyName]; + } + throw new PlDBNoSuchKeyException($keyName, $this); + } + + private function bestKeyFields(PlDBTableEntry $entry, $allowMultiple) + { + if ($this->hasKeyField($entry, $this->primaryKey)) { + return $this->primaryKey; + } + foreach ($this->uniqueKeys as $fields) { + if ($this->hasKeyField($entry, $fields)) { + return $fields; + } + } + if ($allowMultiple) { + foreach ($this->multipleKeys as $fields) { + if ($this->hasKeyField($entry, $fields)) { + return $fields; + } + } + } + return $this->primaryKey; + } + + public function key(PlDBTableEntry $entry, array $keyFields) { $key = array(); - foreach ($this->keyFields as $field) { + foreach ($keyFields as $field) { if (!isset($entry->$field)) { throw new PlDBIncompleteEntryDescription($field, $this); } else { @@ -218,10 +420,15 @@ class PlDBTable return implode('-', $key); } - private function buildKeyCondition(PlDBTableEntry $entry, $allowIncomplete) + public function primaryKey(PlDBTableEntry $entry) + { + return $this->key($this->keyFields(self::PRIMARY_KEY)); + } + + private function buildKeyCondition(PlDBTableEntry $entry, array $keyFields, $allowIncomplete) { $condition = array(); - foreach ($this->keyFields as $field) { + foreach ($keyFields as $field) { if (!isset($entry->$field)) { if (!$allowIncomplete) { throw new PlDBIncompleteEntryDescription($field, $this); @@ -237,44 +444,146 @@ class PlDBTable { $result = XDB::rawFetchOneAssoc('SELECT * FROM ' . $this->table . ' - WHERE ' . $this->buildKeyCondition($entry, false)); + WHERE ' . $this->buildKeyCondition($entry, + $this->bestKeyFields($entry, false), + false)); if (!$result) { return false; } return $entry->fillFromDBData($result); } - public function iterateOnEntry(PlDBTableEntry $entry) + public function iterateOnCondition(PlDBTableEntry $entry, $condition, $sortField) { + if (empty($sortField)) { + $sortField = $this->primaryKey; + } + if (!is_array($sortField)) { + $sortField = array($sortField); + } + $sort = ' ORDER BY ' . implode(', ', $sortField); $it = XDB::rawIterator('SELECT * FROM ' . $this->table . ' - WHERE ' . $this->buildKeyCondition($entry, true)); + WHERE ' . $condition . ' + ' . $sort); return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData')); } - public function updateEntry(PlDBTableEntry $entry) + public function iterateOnEntry(PlDBTableEntry $entry, $sortField) { - $values = array(); - foreach ($this->mutableFields as $field) { - if ($entry->hasChanged($field)) { - $values[] = XDB::format($field . ' = {?}', $entry->$field); + return $this->iterateOnCondition($entry, + $this->buildKeyCondition($entry, + $this->bestKeyFields($entry, true), + true), + $sortField); + } + + const SAVE_INSERT_MISSING = 0x01; + const SAVE_UPDATE_EXISTING = 0x02; + const SAVE_IGNORE_DUPLICATE = 0x04; + public function saveEntries(array $entries, $flags) + { + $flags &= (self::SAVE_INSERT_MISSING | self::SAVE_UPDATE_EXISTING | self::SAVE_IGNORE_DUPLICATE); + Platal::assert($flags != 0, "Hey, the flags ($flags) here are so stupid, don't know what to do"); + if ($flags == self::SAVE_UPDATE_EXISTING) { + foreach ($entries as $entry) { + $values = array(); + foreach ($this->mutableFields as $field) { + if ($entry->hasChanged($field)) { + $values[] = XDB::format($field . ' = {?}', $entry->$field); + } + } + if (count($values) > 0) { + XDB::rawExecute('UPDATE ' . $this->table . ' + SET ' . implode(', ', $values) . ' + WHERE ' . $this->buildKeyCondition($entry, + $this->keyFields(self::PRIMARY_KEY), + false)); + } + } + } else { + $fields = new PlFlagSet(); + foreach ($entries as $entry) { + foreach ($this->schema as $field=>$type) { + if ($type->inPrimaryKey || $entry->hasChanged($field)) { + $fields->addFlag($field); + } + } + } + if (count($fields->export()) > 0) { + foreach ($entries as $entry) { + $v = array(); + foreach ($fields as $field) { + $v[$field] = XDB::escape($entry->$field); + } + $values[] = '(' . implode(', ', $v) . ')'; + } + + $query = $this->table . ' (' . implode(', ', $fields->export()) . ') + VALUES ' . implode(",\n", $values); + if (($flags & self::SAVE_UPDATE_EXISTING)) { + $update = array(); + foreach ($this->mutableFields as $field) { + if (isset($values[$field])) { + $update[] = "$field = VALUES($field)"; + } + } + if (count($update) > 0) { + $query = 'INSERT INTO ' . $query; + $query .= "\n ON DUPLICATE KEY UPDATE " . implode(', ', $update); + } else { + $query = 'INSERT IGNORE INTO ' . $query; + } + } else if (($flags & self::SAVE_IGNORE_DUPLICATE)) { + $query = 'INSERT IGNORE INTO ' . $query; + } else { + $query = 'INSERT INTO ' . $query; + } + XDB::rawExecute($query); + if (count($entries) == 1) { + $id = XDB::insertId(); + if ($id) { + $entry = end($entries); + foreach ($this->primaryKey as $field) { + if ($this->schema[$field]->autoIncrement) { + $entry->$field = $id; + break; + } + } + } + } } } - if (count($values) > 0) { - XDB::rawExecute('UPDATE ' . $this->table . ' - SET ' . implode(', ', $values) . ' - WHERE ' . $this->buildKeyCondition($entry, false)); + } + + public function deleteEntry(PlDBTableEntry $entry, $allowIncomplete) + { + XDB::rawExecute('DELETE FROM ' . $this->table . ' + WHERE ' . $this->buildKeyCondition($entry, + $this->bestKeyFields($entry, $allowIncomplete), + $allowIncomplete)); + } + + public function exportEntry(PlDBTableEntry $entry) + { + $export = array(); + foreach ($this->schema as $key=>$field) { + $value = $entry->$key; + if ($value instanceof PlExportable) { + $value = $value->export(); + } + $export[$key] = $value; } + return $export; } public static function get($name) { - var_dump('blah'); return new PlDBTable($name); } } -class PlDBTableEntry extends PlAbstractIterable +class PlDBTableEntry extends PlAbstractIterable implements PlExportable { private $table; private $changed; @@ -294,6 +603,28 @@ class PlDBTableEntry extends PlAbstractIterable $this->changed = new PlFlagSet(); } + /** Register a custom formatter for a field. + * + * A formatter can be used to perform on-the-fly conversion from db storage to a user-friendly format. + * For example, if you have a textual field that contain json, you can use a JSonFieldFormatter on this + * field to perform automatic decoding when reading from the database (or when assigning the field) + * and automatic json_encoding when storing the object back to the db. + */ + protected function registerFieldFormatter($field, $formatterClass) + { + $this->table->registerFieldFormatter($field, $formatterClass); + } + + /** Register a custom validator for a field. + * + * A validator perform a pre-filter on the value of a field. As opposed to the formatters, it does + * not affects how the value is stored in the database. + */ + protected function registerFieldValidator($field, $validatorClass) + { + $this->table->registerFieldValidator($field, $validatorClass); + } + /** This hook is called when the entry is going to be updated in the db. * * A typical usecase is a class that stores low-level representation of @@ -307,6 +638,26 @@ class PlDBTableEntry extends PlAbstractIterable return true; } + /** This hook is called when the entry has been save in the database. + * + * It can be used to perform post-actions on save like storing extra data + * in database or sending a notification. + */ + protected function postSave() + { + } + + /** This hook is called when the entry is going to be deleted from the db. + * + * Default behavior is to call preSave(). + * + * @return true in case of success. + */ + protected function preDelete() + { + return $this->preSave(); + } + /** This hook is called when the entry has just been fetched from the db. * * This is the counterpart of @ref preSave and a typical use-case is the conversion @@ -368,6 +719,15 @@ class PlDBTableEntry extends PlAbstractIterable return $this->postFetch(); } + public function copy(PlDBTableEntry $other) + { + Platal::assert($this->table == $other->table, + "Trying to fill an entry of table {$this->table->table} with content of {$other->table->table}."); + $this->changed = $other->changed; + $this->fetched = $other->fetched; + $this->data = $other->data; + } + public function cloneAndFillFromDBData(array $data) { $clone = clone $this; @@ -380,20 +740,81 @@ class PlDBTableEntry extends PlAbstractIterable return $this->table->fetchEntry($this); } - public function iterate() + public function iterate($sortField = null) { - return $this->table->iterateOnEntry($this); + return $this->table->iterateOnEntry($this, $sortField); } - public function save() + public function iterateOnCondition($condition, $sortField = null) { - if (!$this->preSave()) { - return false; + return $this->table->iterateOnCondition($this, $condition, $sortField); + } + + public function save($flags) + { + return self::saveBatch(array($this), $flags); + } + + public function update($insertMissing = false) + { + $flags = PlDBTable::SAVE_UPDATE_EXISTING; + if ($insertMissing) { + $flags = PlDBTable::SAVE_INSERT_MISSING; + } + return $this->save($flags); + } + + public function insert($allowUpdate = false) + { + $flags = PlDBTable::SAVE_INSERT_MISSING; + if ($allowUpdate) { + $flags |= PlDBTable::SAVE_UPDATE_EXISTING; + } + return $this->save($flags); + } + + public function delete() + { + if (!$this->preDelete()) { + return 0; + } + return $this->table->deleteEntry($this, true); + } + + public function export() + { + return $this->table->exportEntry($this); + } + + protected static function saveBatch($entries, $flags) + { + $table = null; + foreach ($entries as $entry) { + if (is_null($table)) { + $table = $entry->table; + } else { + Platal::assert($table === $entry->table, "Cannot save batch of entries of different kinds"); + } + if (!$entry->preSave()) { + return false; + } + } + $table->saveEntries($entries, $flags); + foreach ($entries as $entry) { + $entry->changed->clear(); + $entry->postSave(); } - $this->table->updateEntry($this); - $this->changed->clear(); return true; } + + public static function insertBatch($entries, $allowUpdate = false) + { + $flags = PlDBTable::SAVE_INSERT_MISSING; + if ($allowUpdate) { + $flags |= PlDBTable::SAVE_UPDATE_EXISTING; + } + return self::saveBatch($entries, $flags); + } } // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: