diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..9857e47
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,17 @@
+# For more information about the properties used in this file,
+# please see the EditorConfig documentation:
+# http://editorconfig.org
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = tab
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[{*.yml,package.json}]
+indent_size = 2
+
+# The indent size used in the package.json file cannot be changed:
+# https://github.com/npm/npm/pull/3180#issuecomment-16336516
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..fad44cc
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,3 @@
+
+ Deny from all
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0b99800
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,17 @@
+Copyright (c) 2016, Zauberfisch - www.zauberfisch.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of SilverStripe nor the names of its contributors may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..818dc06
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+# SilverStripe SerializedDataObject module
+
+SilverStripe database field that allows saving arbitrary data in a single db field using serialization
+
+## Maintainer Contact
+
+* Zauberfisch
+
+## Requirements
+
+* silverstripe/framework >=3.1
+
+## Installation
+
+* `composer require "zauberfisch/silverstripe-serialized-dataobject"`
+* rebuild manifest (flush)
+
+## Documentation
+
+haha, right
diff --git a/_config/.gitignore b/_config/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/code/SerializedDBField.php b/code/SerializedDBField.php
new file mode 100644
index 0000000..5c705c2
--- /dev/null
+++ b/code/SerializedDBField.php
@@ -0,0 +1,73 @@
+value && is_string($this->value) && $this->value[0] == "'" && $this->value[strlen($this->value) - 1] == "'") {
+ $this->value = substr($this->value, 1, -1);
+ }
+ if (!$this->value) {
+ $this->value = $this->nullValue();
+ } elseif (is_string($this->value)) {
+ $this->value = unserialize($this->value);
+ }
+ return $this->value;
+ }
+
+ /**
+ * @param SerializedDataList|SerializedDataObject|SerializedDBField|array|null|string $value
+ * @param null $record
+ * @param bool|true $markAsChanged
+ */
+ public function setValue($value, $record = null, $markAsChanged = true) {
+ $this->isChanged = $this->isChanged || $markAsChanged;
+ if (is_a($value, __CLASS__)) {
+ $value = $value->getValue();
+ }
+ parent::setValue($value, $record);
+ }
+
+ public function isChanged() {
+ return $this->isChanged;
+ }
+
+ public function prepValueForDB($value) {
+ if (is_a($value, __CLASS__)) {
+ $value = $value->getValue();
+ }
+ if (is_a($value, 'Serializable')) {
+ $value = serialize($value);
+ }
+ if (is_array($value)) {
+ $value = serialize($value);
+ }
+ return parent::prepValueForDB($value);
+ }
+
+ public function requireField() {
+ // keep using deprecated DB::requireField() for 3.1 compatibility
+ /** @noinspection PhpDeprecationInspection */
+ DB::requireField($this->tableName, $this->name, [
+ 'type' => 'text',
+ 'parts' => [
+ 'datatype' => 'mediumtext',
+ 'character set' => 'utf8',
+ 'collate' => 'utf8_general_ci',
+ 'arrayValue' => $this->arrayValue,
+ ],
+ ]);
+ }
+
+ public function __toString() {
+ return $this->prepValueForDB($this->getValue());
+ }
+}
diff --git a/code/SerializedDBFieldHasMany.php b/code/SerializedDBFieldHasMany.php
new file mode 100644
index 0000000..25445d5
--- /dev/null
+++ b/code/SerializedDBFieldHasMany.php
@@ -0,0 +1,12 @@
+ $this->class,
+ 'items' => $this->toArray(),
+ ];
+ }
+
+ public function serialize() {
+ return serialize($this->toArray());
+ }
+
+ public function unserialize($serialized) {
+ $this->items = unserialize($serialized);
+ }
+
+ public function __toString() {
+ return "'" . $this->serialize() . "'";
+ }
+
+ public function __construct(array $items = []) {
+ array_walk($items, function ($item) {
+ if (!is_a($item, 'SerializedDataObject')) {
+ throw new InvalidArgumentException();
+ }
+ });
+ parent::__construct($items);
+ }
+
+ public function push($item) {
+ if (!is_a($item, 'SerializedDataObject')) {
+ throw new InvalidArgumentException();
+ }
+ parent::push($item);
+ }
+
+ public function add($item) {
+ $this->push($item);
+ }
+
+ public function remove($item) {
+ if (!is_a($item, 'SerializedDataObject')) {
+ throw new InvalidArgumentException();
+ }
+ parent::remove($item);
+ }
+
+ public function replace($item, $with) {
+ if (!is_a($item, 'SerializedDataObject') || !is_a($with, 'SerializedDataObject')) {
+ throw new InvalidArgumentException();
+ }
+ parent::replace($item, $with);
+ }
+
+ public function merge($item) {
+ if (!is_a($item, 'SerializedDataObject')) {
+ throw new InvalidArgumentException();
+ }
+ parent::merge($item);
+ }
+
+ public function unshift($item) {
+ if (!is_a($item, 'SerializedDataObject')) {
+ throw new InvalidArgumentException();
+ }
+ parent::unshift($item);
+ }
+}
diff --git a/code/SerializedDataObject.php b/code/SerializedDataObject.php
new file mode 100644
index 0000000..06a6dc5
--- /dev/null
+++ b/code/SerializedDataObject.php
@@ -0,0 +1,204 @@
+ $this->class,
+ 'fieldsData' => $this->fieldsData,
+ 'listsData' => $this->listsData,
+ ]);
+ }
+
+ public function serialize() {
+ return serialize([
+ 'fieldsData' => $this->fieldsData,
+ 'listsData' => $this->listsData,
+ ]);
+ }
+
+ public function unserialize($serialized) {
+ $data = unserialize($serialized);
+ $this->class = get_class($this);
+ $this->fieldsData = isset($data['fieldsData']) ? $data['fieldsData'] : [];
+ $this->listsData = isset($data['listsData']) ? $data['listsData'] : [];
+ }
+
+ public function __get($fieldName) {
+ return $this->getField($fieldName);
+ }
+
+ public function __set($fieldName, $value) {
+ return $this->setField($fieldName, $value);
+ }
+
+// public function __call($method, $arguments) {
+// if ($this->hasList($method)) {
+// return $this->getList($method);
+// }
+// // if (strlen($method) > 3) {
+// // list($prefix, $fieldName) = str_split($method, 3);
+// // if ($this->hasField($fieldName)) {
+// // return $this->{$prefix . "Field"}($arguments);
+// // }
+// // }
+// return parent::__call($method, $arguments);
+// }
+
+ public function defineMethods() {
+ parent::defineMethods();
+ // TODO how to handle method name collisions?
+ foreach (static::config()->fields as $field) {
+ $this->createMethod("set$field", sprintf('return $obj->setField("%s", $args[0]);', $field));
+ $this->createMethod("get$field", sprintf('return $obj->getField("%s");', $field));
+ //$this->addWrapperMethod("set$field", 'setField');
+ //$this->addWrapperMethod("get$field", 'getField');
+ }
+ foreach (static::config()->lists as $field) {
+ $this->createMethod("set$field", sprintf('return $obj->setList("%s", $args[0]);', $field));
+ $this->createMethod("get$field", sprintf('return $obj->getList("%s");', $field));
+ //$this->addWrapperMethod("set$field", 'setField');
+ //$this->addWrapperMethod("get$field", 'getField');
+ }
+ }
+
+ public function hasField($name) {
+ return in_array($name, static::config()->fields);
+ }
+
+ public function getField($name) {
+ if ($this->hasField($name)) {
+ if (isset($this->fieldsData[$name])) {
+ return $this->fieldsData[$name];
+ }
+ return null;
+ }
+ throw new Exception("Could not find field '$name'.");
+ }
+
+ public function setField($name, $value) {
+ if ($this->hasField($name)) {
+ $this->fieldsData[$name] = $value;
+ return $this;
+ }
+ throw new Exception("Could not find field '$name'.");
+ }
+
+ public function hasList($name) {
+ return in_array($name, static::config()->lists);
+ }
+
+ public function getList($name) {
+ if ($this->hasList($name)) {
+ if (!isset($this->listsData[$name])) {
+ $this->listsData[$name] = new SerializedDataList();
+ }
+ return $this->listsData[$name];
+ }
+ throw new Exception("Could not find field '$name'.");
+ }
+
+ public function setList($name, SerializedDataList $value) {
+ if ($this->hasList($name)) {
+ $this->listsData[$name] = $value;
+ return $this;
+ }
+ throw new Exception("Could not find field '$name'.");
+ }
+
+ /**
+ * @param array $data
+ * @return $this
+ * @throws Exception
+ */
+ public function update($data) {
+ foreach (array_merge(static::config()->lists, static::config()->fields) as $name) {
+ if (isset($data[$name])) {
+ $value = $data[$name];
+ if (is_a($value, 'SerializedDataList')) {
+ if ($this->hasList($name)) {
+ $this->setList($name, $value);
+ }
+ } elseif ($this->hasField($name)) {
+ $this->setField($name, $value);
+ }
+ }
+ }
+ return $this;
+ }
+
+ private static $_cache_field_labels = [];
+
+ protected function i18nFields() {
+ $fields = [];
+ $ancestry = array_reverse(ClassInfo::ancestry($this->class));
+ if ($ancestry) {
+ foreach ($ancestry as $ancestorClass) {
+ if ($ancestorClass == __CLASS__) {
+ break;
+ }
+ $fields[$ancestorClass] = [];
+ foreach ([
+ 'field' => (array)Config::inst()->get($ancestorClass, 'field', Config::UNINHERITED),
+ 'list' => (array)Config::inst()->get($ancestorClass, 'list', Config::UNINHERITED),
+ ] as $type => $attrs) {
+ $fields[$ancestorClass][$type] = [];
+ foreach ($attrs as $name => $spec) {
+ $fields[$ancestorClass][$type][$name] = FormField::name_to_label($name);
+ }
+ }
+ }
+ }
+ return $fields;
+ }
+
+ public function fieldLabels() {
+ $cacheKey = $this->class;
+ if (!isset(self::$_cache_field_labels[$cacheKey])) {
+ $labels = [];
+ foreach ($this->i18nFields() as $className => $types) {
+ foreach ($types as $type => $defaultLabels) {
+ foreach ($defaultLabels as $name => $defaultValue) {
+ $labels["{$type}_$name"] = _t("$className.{$type}_$name", $defaultValue);
+ if (!isset($labels[$name])) {
+ $labels[$name] = $labels["{$type}_$name"];
+ }
+ }
+ }
+ }
+ self::$_cache_field_labels[$cacheKey] = $labels;
+ }
+ return self::$_cache_field_labels[$cacheKey];
+ }
+
+ public function fieldLabel($name) {
+ $labels = $this->fieldLabels();
+ return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
+ }
+
+ public function provideI18nEntities() {
+ $entities = [];
+ foreach ($this->i18nFields() as $className => $types) {
+ foreach ($types as $type => $defaultLabels) {
+ foreach ($defaultLabels as $name => $defaultValue) {
+ $entities["$className.{$type}_$name"] = $defaultValue;
+ }
+ }
+ }
+ return $entities;
+ }
+
+ public function __toString() {
+ return serialize($this);
+ }
+}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..f408cb0
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,17 @@
+{
+ "name": "zauberfisch/silverstripe-serialized-dataobject",
+ "description": "SilverStripe database field that allows saving arbitrary data in a single db field using serialization",
+ "type": "silverstripe-module",
+ "keywords": ["silverstripe", "serialize", "dataobject", "dbfield", "multivalue"],
+ "license": "BSD-3-Clause",
+ "authors": [{
+ "name": "Zauberfisch",
+ "email": "code@zauberfisch.at"
+ }],
+ "require": {
+ "silverstripe/framework": "~3.1"
+ },
+ "extra": {
+ "installer-name": "serialized-dataobject"
+ }
+}