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" + } +}