Remove unused reference to $globals in PlPage
[platal.git] / classes / pldbtableentry.php
index 8fe2942..1ab5d4c 100644 (file)
@@ -1,6 +1,6 @@
 <?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   *
@@ -74,6 +74,9 @@ class PlDBTableField
     public $defaultValue;
     public $autoIncrement;
 
+    private $validator;
+    private $formatter;
+
     public function __construct(array $column)
     {
         $this->name = $column['Field'];
@@ -97,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)) {
@@ -107,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());
@@ -132,54 +153,119 @@ 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') {
-            $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]);
+    }
 }
 
 
@@ -263,6 +349,17 @@ 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;
@@ -384,34 +481,46 @@ class PlDBTable
     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) {
@@ -420,23 +529,26 @@ class PlDBTable
                         }
                     }
                     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;
+                            }
                         }
                     }
                 }
@@ -452,13 +564,26 @@ class PlDBTable
                                                                   $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;
@@ -478,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
@@ -575,7 +722,7 @@ class PlDBTableEntry extends PlAbstractIterable
     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;
@@ -605,13 +752,7 @@ class PlDBTableEntry extends PlAbstractIterable
 
     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)
@@ -639,6 +780,41 @@ class PlDBTableEntry extends PlAbstractIterable
         }
         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: