<?php
/***************************************************************************
- * Copyright (C) 2003-2010 Polytechnique.org *
+ * Copyright (C) 2003-2011 Polytechnique.org *
* http://opensource.polytechnique.org/ *
* *
* This program is free software; you can redistribute it and/or modify *
public $defaultValue;
public $autoIncrement;
+ private $validator;
+ private $formatter;
+
public function __construct(array $column)
{
$this->name = $column['Field'];
}
}
+ public function registerFormatter($class)
+ {
+ $this->formatter = $class;
+ }
+
+ public function registerValidator($class)
+ {
+ $this->validator = $class;
+ }
+
public function format($value, $badNullFallbackToDefault = false)
{
if (is_null($value)) {
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());
}
/* 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') {
- $date = $value;
- $value = make_datetime($value);
- if (is_null($value)) {
- throw new PlDBBadValueException($date, $this, 'value is expected to be a date/time, ' . $date . ' given');
- }
- if ($this->type == 'date') {
- $value = new DateFormatter($value, 'Y-m-d');
- } else if ($this->type == 'datetime') {
- $value = new DateFormatter($value, 'Y-m-d H:i:s');
- } else {
- $value = new DateFormatter($value, 'U');
- }
+ return new DateFieldFormatter($this, $value);
}
return $value;
}
}
-class DateFormatter implements XDBFormat
+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(DateTime $date, $storageFormat)
+ public function __construct(PlDBTableField $field, $date)
{
- $this->datetime = $date;
- $this->storageFormat = $storageFormat;
+ $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->datetime->format($this->storageFormat));
+ 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]);
+ }
}
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;
const SAVE_INSERT_MISSING = 0x01;
const SAVE_UPDATE_EXISTING = 0x02;
const SAVE_IGNORE_DUPLICATE = 0x04;
- public function saveEntry(PlDBTableEntry $entry, $flags)
+ 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) {
- $values = array();
- foreach ($this->mutableFields as $field) {
- if ($entry->hasChanged($field)) {
- $values[] = XDB::format($field . ' = {?}', $entry->$field);
+ 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));
}
- }
- if (count($values) > 0) {
- XDB::rawExecute('UPDATE ' . $this->table . '
- SET ' . implode(', ', $values) . '
- WHERE ' . $this->buildKeyCondition($entry,
- $this->keyFields(self::PRIMARY_KEY),
- false));
}
} else {
- $values = array();
- foreach ($this->schema as $field=>$type) {
- if ($entry->hasChanged($field)) {
- $values[$field] = XDB::escape($entry->$field);
+ $fields = new PlFlagSet();
+ foreach ($entries as $entry) {
+ foreach ($this->schema as $field=>$type) {
+ if ($type->inPrimaryKey || $entry->hasChanged($field)) {
+ $fields->addFlag($field);
+ }
}
}
- if (count($values) > 0) {
- $query = $this->table . ' (' . implode(', ', array_keys($values)) . ')
- VALUES (' . implode(', ', $values) . ')';
+ 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 (count($update) > 0) {
- $query = 'INSERT ' . $query;
+ $query = 'INSERT INTO ' . $query;
$query .= "\n ON DUPLICATE KEY UPDATE " . implode(', ', $update);
} else {
- $query = 'INSERT IGNORE ' . $query;
+ $query = 'INSERT IGNORE INTO ' . $query;
}
} else if (($flags & self::SAVE_IGNORE_DUPLICATE)) {
- $query = 'INSERT IGNORE ' . $query;
+ $query = 'INSERT IGNORE INTO ' . $query;
} else {
- $query = 'INSERT ' . $query;
+ $query = 'INSERT INTO ' . $query;
}
XDB::rawExecute($query);
- $id = XDB::insertId();
- if ($id) {
- foreach ($this->primaryKey as $field) {
- if ($this->schema[$field]->autoIncrement) {
- $entry->$field = $id;
- break;
+ 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;
+ }
}
}
}
$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)
{
return new PlDBTable($name);
}
}
-class PlDBTableEntry extends PlAbstractIterable
+class PlDBTableEntry extends PlAbstractIterable implements PlExportable
{
private $table;
private $changed;
$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
public function copy(PlDBTableEntry $other)
{
Platal::assert($this->table == $other->table,
- "Trying to fill an entry of table {$this->table} with content of {$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 save($flags)
{
- if (!$this->preSave()) {
- return false;
- }
- $this->table->saveEntry($this, $flags);
- $this->changed->clear();
- $this->postSave();
- return true;
+ return self::saveBatch(array($this), $flags);
}
public function update($insertMissing = false)
}
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();
+ }
+ 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: