Enh: file fields: metadata (#6594)

* Add `File.metadata`

* Mark experimental

* fixup! Add `File.metadata`

* Use `isModified()` rather than `count` to evaluate `null` before saving.
This commit is contained in:
Martin Rüegg 2023-10-05 10:26:30 +02:00 committed by GitHub
parent 3d107cb4de
commit 06d5114cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1929 additions and 5 deletions

View File

@ -3,6 +3,7 @@ HumHub Changelog
1.15.0-beta.2 (Unreleased)
--------------------------
- Enh #6594: Add field `file.metadata`
- Enh #6593: Add field `file.sort_order`
- Enh #6592: Add field `file.state`
- Enh #6591: Add field `file.category`

View File

@ -52,3 +52,19 @@ class __Application {
class __WebApplication extends \humhub\components\Application
{
}
if (!class_exists(WeakReference::class)) {
class WeakReference
{
/* Methods */
public static function create(object $object): self
{
return new static();
}
public function get(): ?object
{
return null;
}
}
}

View File

@ -98,9 +98,11 @@ trait InvalidArgumentExceptionTrait
$int = filter_var($this->parameter, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($int === null) {
$parameter = preg_replace('@^(\.\.\.)?\$?@', '$1\$', $this->parameter);
return $this->parameter === null
? 'Unknown argument'
: "Argument \$" . ltrim($this->parameter, '$');
: "Argument " . $parameter;
}
return 'Argument #' . $int;

View File

@ -0,0 +1,907 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\libs;
use __PHP_Incomplete_Class;
use ArrayAccess;
use ArrayIterator;
use Countable;
use humhub\exceptions\InvalidArgumentTypeException;
use humhub\exceptions\InvalidArgumentValueException;
use humhub\exceptions\InvalidConfigTypeException;
use OutOfBoundsException;
use ReflectionClass;
use ReflectionException;
use ReflectionObject;
use ReflectionProperty;
use SeekableIterator;
use Serializable;
use Stringable;
use Traversable;
use Yii;
use yii\base\Arrayable;
use yii\base\ArrayableTrait;
use yii\helpers\ArrayHelper;
/**
* WARNING: This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
* ---
*
* This class provides an object that can have dynamic properties (like \stdClass) but with some additional features:
* - the properties can also be access in the array-like manner, \
* i.e. `$object['property']` for `$object->property`
* - `count()` can be used to get the number of properties
* - the object can be cleared \
* with `static:clear()` or `static::addValue(null)`
* - add multiple values in as an array \
* with `static::addValue($property1 => $value1, $property2 => $value2, ...)`
* - add multiple array of value sets which will be merged first \
* with `static::addValue([$property1 => $value1, ...], [$property2 => $value2, ...], ...)`
* - allows to verify if the entire object or a particular property has been modified \
* with `static::isModified()` \
* and `static::isFieldModified($field)`
* - allows to retrieve the (un)modified fields \
* with `static::fieldsModified(true|false)` (`null` will return all fields)
* - the internal array pointer can be moved \
* with `static::seek($pos)`
* - the object is stringable (which by default uses serialize)
* - provides a factory
* with `\humhub\libs\StdClass::create()`
* - the serialization is optimized to
* - reduce data by only exporting modified versions of self, and using pre-defined short property keys
* - provide format versioning
* - only allow deserialization of self and subclasses of self
*
* @since 1.15 This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
* @internal (ToDo)
* @see static::addValues()
* @see static::__serialize()
* @see static::unserialize()
*/
class StdClass extends \stdClass implements ArrayAccess, Stringable, SeekableIterator, Countable, Serializable, Arrayable
{
use ArrayableTrait;
public const SERIALIZE_FORMAT = 1;
protected const SERIALIZE_VALUE__VERSION = 'v';
protected const SERIALIZE_VALUE__DATA = '_0';
/**
* List of extracted properties from the object to be unserialized.
* - For optional properties, just add the property name to the list
* - For *required* properties, use the following syntax: `[$propertyName => true]`
*/
protected const UNSERIALIZE_REQUIRED_VALUES = [
self::SERIALIZE_VALUE__VERSION => true,
self::SERIALIZE_VALUE__DATA,
];
/**
* @var \stdClass|null
*/
protected static ?\stdClass $validatedObject = null;
/**
* @param array|traversable|string|null ...$args see class description
*
* @return static
* @throws InvalidConfigTypeException
* @see \humhub\libs\StdClass
* @noinspection MagicMethodsValidityInspection
* @noinspection PhpUnnecessaryFullyQualifiedNameInspection
*/
public function __construct(...$args)
{
return $this->addValues(...$args);
}
public function __get($name)
{
$this->validatePropertyName($name, __METHOD__);
/** @noinspection PhpExpressionAlwaysNullInspection */
return $this->$name ?? null;
}
public static function isSerializing(?self $object, bool $end = false): bool
{
static $current;
if ($current === null) {
if ($object) {
$current = $object;
}
return false;
}
if ($end && $object && $object === $current) {
$current = null;
return false;
}
return true;
}
/**
* @param $name
* @param $value
*
* @return $this
*/
public function __set($name, $value)
{
$this->validatePropertyName($name, __METHOD__);
$this->$name = $value;
return $this;
}
public function __isset($name)
{
return property_exists($this, $name);
}
public function __unset($name)
{
$name = $this->validatePropertyName($name, __METHOD__);
unset($this->$name);
return $this;
}
public function __toString()
{
return serialize($this);
}
public function __serialize(): array
{
$return = [
self::SERIALIZE_VALUE__VERSION => self::SERIALIZE_FORMAT,
];
$fields = $this->fieldsModified(true);
if ($fields !== null) {
$fields = array_fill_keys($fields, null);
foreach ($fields as $field => &$value) {
$value = $this->$field;
}
unset($value);
$return[self::SERIALIZE_VALUE__DATA] = &$fields;
}
return $return;
}
/**
* @param array|\stdClass $serialized
*
* @return self
* @throws InvalidArgumentTypeException|InvalidConfigTypeException
* @noinspection MagicMethodsValidityInspection
*/
public function __unserialize($serialized)
{
$valid = $this->validateSerializedInput($serialized);
// clear only after validation was successful
$this->clear();
if (!$valid || !property_exists($serialized, self::SERIALIZE_VALUE__DATA)) {
return $this;
}
return $this->addValues($serialized->{self::SERIALIZE_VALUE__DATA});
}
/**
* @see static::__serialize()
*/
public function serialize(): string
{
return serialize($this);
}
/**
* This function is automatically called when you run `unserialize($string)' (where `$string` includes this class)
* or
* - `new static($string)`
* - `$object->unserialize($string)`
* In any case, `$string` MUST start with the object definition of this class and MUST NOT contain any object other
* than this class (self::class) or `\stdClass`
*
* @param string $serialized
*
* @return $this
* @throws InvalidConfigTypeException
*/
public function unserialize($serialized): self
{
if (!is_string($serialized)) {
throw new InvalidArgumentTypeException(
'$serialized',
["string"],
$serialized
);
}
/**
* If `self::class` and `static::class` are the same, they are merged into one key. That's intentional.
*
* @noinspection PhpDuplicateArrayKeysInspection
*/
$allowedClasses = [
self::class => self::class,
static::class => static::class,
\stdClass::class => null,
];
/**
* @var string|null $signature Will contain the signature found for the top-level object
*
* @noinspection AlterInForeachInspection
*/
foreach ($allowedClasses as $class => &$signature) {
if ($signature === null) {
$allowedClasses = array_filter($allowedClasses);
throw new InvalidArgumentValueException(
'$serialized',
sprintf("string starting with '%s'", implode(' or ', $allowedClasses)),
$serialized
);
}
$signature = sprintf('O:%s:"%s"', strlen($class), $class);
if (str_starts_with($serialized, $signature)) {
break;
}
}
// replace the definition of the top-level object to be a \stdClass, so we can internally unserialize it and
// retrieve the values needed to set up this instance
$serialized = substr_replace($serialized, 'O:8:"stdClass"', 0, strlen($signature));
// get the basic values of the serialized object
$clone = unserialize($serialized, ['allowed_classes' => array_keys($allowedClasses)]);
// now translate that into the real structure
return $this->__unserialize($clone);
}
/**
* @return static
* @throws InvalidConfigTypeException
* @see static::__construct()
* @noinspection PhpParamsInspection
* @noinspection PhpUnnecessaryFullyQualifiedNameInspection
*/
public static function create(...$args): self
{
return new static(...$args);
}
public function clear(): self
{
foreach ((new ReflectionObject($this))->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
unset($this->{$property->getName()});
}
return $this;
}
/**
* This method allows modifying the values of the current object:
* - `static::addValue()`: nothing happens.
* This is useful for the case where you do `static::addValue(...$someArray)`, where `$someArray` is an empty
* array.
* - `static::addValue(null)`: This resets the entire set of stored values!
* This is equivalent to calling `static::clear()`
* - `static::addValue($serializedString)`: Initializes the object with values from `$serializedString`.
* This is equivalent to calling `static::unserialize()`
* - `static::addValue(array|Traversable $set_1, ...)`:
* Any number of arrays or objects implementing `\Traversable` - allowing `foreach($argument as $property =>
* $value)`. If more than one set is provided, they are merged according to \`yii\helpers\BaseArrayHelper::merge()`
* (earlier values taking precedence), e.g.:
* - `static::addValue([$property => $value, ...])`
* - `static::addValue($traversableObject)`
*
* @param ...$args
*
* @return $this
* @throws InvalidArgumentTypeException|InvalidConfigTypeException
* @see static::unserialize();
* @see \yii\helpers\BaseArrayHelper::merge();
* @see static::clear()
*/
public function addValues(...$args): self
{
switch (count($args)) {
case 0:
return $this;
case 1:
$args = reset($args);
if ($args === null) {
return $this->clear();
}
if (is_string($args)) {
return $this->unserialize($args);
}
break;
default:
$args = ArrayHelper::merge(...$args);
}
if (!is_iterable($args)) {
if (is_object($args)) {
$args = ArrayHelper::toArray($args, [], false);
} else {
throw new InvalidArgumentTypeException('...$args', [
'array',
Traversable::class
], $args);
}
}
foreach ($args as $k => $v) {
$this->$k = $v;
}
return $this;
}
public function count(): int
{
return count($this->fields());
}
public function isModified(): bool
{
return $this->fieldsModified(false, true) === null;
}
public function isFieldModified(string $field): ?bool
{
if (!$this->__isset($field)) {
return null;
}
try {
$property = new ReflectionProperty($this, $field);
} catch (ReflectionException $e) {
// Not an actual property. It may be the result of a getField() getter. So assume, it has been set.
return true;
}
// check if the property has been defined in the class, or dynamically
if (!$property->isDefault()) {
// we always assume dynamically assigned properties as changed
return true;
}
// static properties are ignored
if ($property->isStatic()) {
return null;
}
$data = $this->$field;
/**
* this only works as of PHP v8
*
* @ToDo remove version check and this comment when when min required version is 8.0
* @noinspection PhpUndefinedMethodInspection
*/
if (PHP_MAJOR_VERSION >= 8 && $property->hasDefaultValue() && $data === $property->getDefaultValue()) {
return false;
}
if ($data instanceof self) {
return $data->isModified();
}
// better be safe than sorry
return true;
}
/**
* @param bool|null $filter If null, both modified and unmodified fields are returned as `[$name => $modified]`
* pairs. If `true`, only modified fields will be returned. The field names are the array values! If `false`,
* only unmodified fields will be returned. The field names are the array values!
*
* @return array
*/
public function &fieldsModified(?bool $filter = null, bool $failOnNoMatch = false): ?array
{
$fields = $this->fields();
foreach ($fields as $field => &$status) {
$status = $this->isFieldModified($field);
if ($failOnNoMatch && $filter !== null && $status !== $filter) {
$fields = null;
return $fields;
}
}
unset($status);
$fields = array_filter($fields, static fn($value) => $filter === null ? $value !== null : $value === $filter);
$fields = count($fields) ? $fields : null;
if ($fields !== null && $filter !== null) {
$fields = array_keys($fields);
}
return $fields;
}
public function getIterator(): ArrayIterator
{
return new ArrayIterator(get_object_vars($this));
}
/**
* @inheritdoc
* @noinspection PhpParamsInspection
*/
public function current()
{
return current($this);
}
/**
* @inheritdoc
* @return string|int|null
* @noinspection PhpParamsInspection
*/
public function key()
{
return key($this);
}
/**
* @inheritdoc
* @noinspection PhpParamsInspection
*/
public function next()
{
next($this);
return $this;
}
public function offsetExists($offset): bool
{
return $this->__isset($offset);
}
public function offsetGet($offset)
{
return $this->__get(
$this->validatePropertyName($offset, __METHOD__, '$offset')
);
}
public function offsetSet($offset, $value)
{
return $this->__set(
$this->validatePropertyName($offset, __METHOD__, '$offset'),
$value
);
}
public function offsetUnset($offset)
{
return $this->__unset(
$this->validatePropertyName($offset, __METHOD__, '$offset')
);
}
/**
* @inheritdoc
* @return static
* @noinspection PhpParamsInspection
* @noinspection PhpMissingReturnTypeInspection
*/
public function rewind()
{
reset($this);
return $this;
}
/**
* @inheritdoc
* @return string|int the current key after seek
* @noinspection PhpParamsInspection
*/
public function seek($position)
{
if (
!is_int($int = $position) && null === $int = filter_var(
$position,
FILTER_VALIDATE_BOOLEAN,
FILTER_NULL_ON_FAILURE
)
) {
throw new InvalidArgumentTypeException('$position', 'int', $position);
}
if ($int < 0) {
$count = $this->count();
$int = $count + $int;
if ($int < 0) {
throw new OutOfBoundsException("Seek position $int is out of range");
}
}
reset($this);
while ($int-- > 0) {
next($this);
if (null === key($this)) {
throw new OutOfBoundsException("Seek position $int is out of range");
}
}
return key($this);
}
/**
* @inheritdoc
* @noinspection PhpParamsInspection
*/
public function valid(): bool
{
return key($this) !== null;
}
/**
* This function is used internally to validate property names
*
* @param $name
* @param string $method
* @param string|array $parameter
*
* @return void
*/
protected function validatePropertyName($name, string $method, string $parameter = '$name'): ?string
{
switch (true) {
case is_string($name):
case is_int($name):
case $name instanceof Stringable:
return $name;
case is_bool($name):
return (int)$name;
}
throw InvalidArgumentTypeException::newInstance(
$parameter,
['string', 'int', 'bool', Stringable::class]
)->setMethodName($method);
}
/**
* @param array|\stdClass $serialized
*
* @return bool|null True if valid array, False if valid object, null on error
* @throws InvalidConfigTypeException
*/
protected function validateSerializedInput(&$serialized, ?array $requiredFields = self::UNSERIALIZE_REQUIRED_VALUES, bool $throw = true): ?bool
{
// this is used to identify already-validated data
self::$validatedObject ??= self::validatedObject();
if ($serialized instanceof self::$validatedObject) {
return true;
}
if (is_array($serialized)) {
$isObject = false;
} elseif (!is_object($serialized) || get_class($serialized) !== \stdClass::class) {
if (!$throw) {
return null;
}
throw new InvalidArgumentTypeException(
'$serialized',
['array', \stdClass::class],
$serialized
);
} else {
$isObject = true;
}
$requiredFields ??= static::UNSERIALIZE_REQUIRED_VALUES;
// check if the default version field (v) is explicitly not wanted
if (($requiredFields[self::SERIALIZE_VALUE__VERSION] ?? true) === null) {
unset($requiredFields[self::SERIALIZE_VALUE__VERSION]);
} else {
$requiredFields[self::SERIALIZE_VALUE__VERSION] = true;
}
// check if the default data field (_0) is explicitly not wanted
if (($requiredFields[self::SERIALIZE_VALUE__DATA] ?? true) === null) {
unset($requiredFields[self::SERIALIZE_VALUE__DATA]);
} else {
$requiredFields[self::SERIALIZE_VALUE__DATA] = false;
}
$result = [];
foreach ($requiredFields as $field => $required) {
if (is_int($field)) {
$result[$required] = false;
} else {
$result[$field] = (bool)$required;
}
}
$requiredFields = $result;
$result = self::validatedObject();
foreach ($requiredFields as $field => $required) {
// allows us to check if the value was set
$value = $this;
/**
* $serialized may be a \stdClass object created by `static::unserialize()`. So check for the required property
* based on the given data type
*
* @see static::unserialize()
*/
if ($isObject) {
if (method_exists($serialized, '__isset') ? $serialized->__isset($field) : property_exists($serialized, $field)) {
$value = &$serialized->{$field};
}
} elseif (is_array($serialized)) {
if (array_key_exists($field, $serialized)) {
$value = &$serialized[$field];
}
}
// check if a value has been retrieved
if ($value === $this) {
if (!$required) {
continue;
}
if (!$throw) {
return false;
}
throw new InvalidArgumentValueException(
sprintf('Required field %s not found in serialized data for %s', $field, static::class)
);
}
if (!$this->validateClassIncomplete($value, $found)) {
if (!$throw) {
return false;
}
throw new InvalidArgumentTypeException(
sprintf('Invalid classes found in serialized data: %s', implode(', ', array_filter($found)))
);
}
$result->$field = &$value;
// destroy reference
unset($value);
}
$serialized = $result;
return true;
}
/**
* @throws InvalidConfigTypeException
*/
protected function validateClassInheritance($class, bool $throw, bool $strict = true): bool
{
if (!is_string($class)) {
if (!$throw) {
return false;
}
throw new InvalidConfigTypeException(sprintf(
"Invalid class property type for %s: %s",
static::class,
get_debug_type($class)
), 1);
}
if (!class_exists($class)) {
if (!$throw) {
return false;
}
throw new InvalidConfigTypeException(sprintf(
"Invalid class name or non-existing class for %s: %s",
static::class,
get_debug_type($class)
), 2);
}
$parentClass = $strict ? static::class : self::class;
if ($class !== $parentClass && !is_subclass_of($class, $parentClass)) {
if (!$throw) {
return false;
}
throw new InvalidConfigTypeException(sprintf(
"Class %s is not a subclass of %s",
get_debug_type($class),
$parentClass
), 3);
}
return true;
}
/**
* @param iterable|\stdClass|null $serialized
* @param array|null $found Output parameter returning
* - `null` if no incomplete class wos found, or
* - a list of class names as key and boolean value indicating that the class has been
* - invalid (true) or
* - valid (false).
* To get a list of all invalid classes, use `array_flip(array_filter((array)$found))`
* @param bool $throw
* @param int|null $recursion recursion will be performed for the level as indicated by this parameter.
* If `null` the default value of 1 will be applied
*
* @return bool
* @throws InvalidConfigTypeException
*/
protected function validateClassIncomplete(&$serialized, ?array &$found = null, bool $throw = true, ?int $recursion = 1): bool
{
$found = [];
$recursion ??= 1;
if ($serialized === null || is_scalar($serialized)) {
return true;
}
foreach ($serialized as $key => $item) {
if (is_scalar($item)) {
continue;
}
$changed = false;
if ($item instanceof __PHP_Incomplete_Class) {
// data of the incomplete class can only be accessed through iteration
$data = [];
$class = null;
foreach ($item as $property => $value) {
if ($property === '__PHP_Incomplete_Class_Name') {
// check if the class name inherits from StdClass
$validateClassInheritance = $this->validateClassInheritance($value, $throw, false);
// Save the result for return value.
// We store `true` for invalid, `false` for valid classes.
// This allows easy filtering invalid classes by using `array_filter($found)`
$found[$value] = !$validateClassInheritance;
if ($validateClassInheritance) {
// would be an alternative, but possibly requires more computational resources:
// $subitem = unserialize(serialize($subitem), ['allowed_classes' => [self::class, static::class, StdClass::class, $value]]);
// create a reflection class to later instantiate the new object
try {
$class = new ReflectionClass($value);
} catch (ReflectionException $e) {
$found[$value] = true;
Yii::warning(
sprintf(
"Reflection Exception occurred while validating metadata! %s",
serialize($item)
),
'File'
);
continue 2;
}
// loop through the other properties
continue;
}
Yii::warning(
sprintf("Invalid metadata found and removed! %s", serialize($item)),
'File'
);
$item = null;
// skip the other properties and go to the next `$subitem`
continue 2;
}
// store the data in our array
$data[$property] = $value;
}
// now create an instance without calling the constructor ...
$item = $class->newInstanceWithoutConstructor();
// ... and initialize the object with the obtained data (as `unserialize` would do)
$item->__unserialize($data);
$changed = true;
$data = null;
$class = null;
} elseif ((is_iterable($item) || $item instanceof \stdClass) && $recursion) {
$incomplete = $this->validateClassIncomplete($item, $newFound, $throw, $recursion - 1);
$found = ArrayHelper::merge($found, (array)$newFound);
if (!$incomplete) {
return false;
}
$changed = true;
}
if ($changed) {
if (is_array($serialized) || $serialized instanceof ArrayAccess) {
$serialized[$key] = $item;
} else {
$serialized->$key = $item;
}
}
}
if ($found === null) {
return true;
}
// since valid classes carry a `false` value, while invalid classes carry a `true` value,
// we can simply filter the array and only invalid classes will remain
return empty(array_filter($found));
}
protected static function validatedObject(): \stdClass
{
if (self::$validatedObject === null) {
self::$validatedObject = new class () extends \stdClass {
};
}
$class = get_class(self::$validatedObject);
return new $class();
}
}

View File

@ -0,0 +1,352 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\libs;
use Error;
use humhub\exceptions\InvalidConfigTypeException;
use ReflectionClass;
use ReflectionException;
use ReflectionObject;
use RuntimeException;
use SplObjectStorage;
use Throwable;
use UnexpectedValueException;
use WeakReference;
use yii\base\UnknownPropertyException;
/**
* WARNING: This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
* ---
*
* @codingStandardsIgnoreFile PSR2.Classes.PropertyDeclaration.Underscore
* @since 1.15 This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
* @internal (ToDo)
*/
class StdClassConfig extends StdClass
{
public const SERIALIZE_FORMAT = 1;
protected const SERIALIZE_VALUE__PARENT_FIXED = '_1';
protected const SERIALIZE_VALUE__CONFIG_FIXED = '_2';
protected const SERIALIZE_VALUE__CLASS = 'class';
protected const UNSERIALIZE_REQUIRED_VALUES = [
self::SERIALIZE_VALUE__CLASS,
self::SERIALIZE_VALUE__PARENT_FIXED,
self::SERIALIZE_VALUE__CONFIG_FIXED,
];
/**
* @var mixed|null
*/
public $default;
// StdClassConfigurable meta-properties
protected bool $__StdClassConfigurable_isFixed = false;
/**
* @var bool Denotes if the parent StdClass is loading, i.e., running the __construct() method
*/
protected bool $__StdClassConfigurable_loading = true;
// StdClassConfig meta-properties
/**
* @var bool Denotes if dynamic properties can be added (false) or the set of properties is fix.
*/
protected bool $__StdClassConfig_isFixed = false;
private static SplObjectStorage $__StdClassConfig_config;
/**
* @var WeakReference Holding a reference to teh parent objet without increasing the reference count
* @see static::getParent()
* @see static::getReflection()
*/
private WeakReference $__StdClassConfig_parent;
/**
* @param StdClassConfigurable $parent
* @param mixed ...$initialValues
*
* @noinspection MagicMethodsValidityInspection
* @throws InvalidConfigTypeException
*/
public function __construct(StdClassConfigurable $parent, ...$initialValues)
{
$this->__StdClassConfig_parent = WeakReference::create($parent);
return parent::__construct(...$initialValues);
}
/**
* @throws UnknownPropertyException
*/
public function __get($name)
{
if ($this->__StdClassConfig_isFixed && !$this->__StdClassConfigurable_loading) {
throw new UnknownPropertyException('Getting unknown property: ' . static::class . '::$' . $name);
}
return parent::__get($name);
}
/**
* @throws UnknownPropertyException
*/
public function __set($name, $value)
{
if ($this->__StdClassConfig_isFixed && !$this->__StdClassConfigurable_loading) {
throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::$' . $name);
}
return parent::__set($name, $value);
}
public function __serialize(): array
{
$data = parent::__serialize();
/**
* ToDo: this can be removed in PHP 8
*
* @see StdClass::isFieldModified()
*/
if (PHP_MAJOR_VERSION < 8) {
if (($data[self::SERIALIZE_VALUE__DATA]['default'] ?? null) === null) {
unset($data[self::SERIALIZE_VALUE__DATA]['default']);
}
if (count($data[self::SERIALIZE_VALUE__DATA]) === 0) {
unset($data[self::SERIALIZE_VALUE__DATA]);
}
}
if ($this->__StdClassConfigurable_isFixed) {
$data[self::SERIALIZE_VALUE__PARENT_FIXED] = $this->__StdClassConfigurable_isFixed;
}
if ($this->__StdClassConfig_isFixed) {
$data[self::SERIALIZE_VALUE__CONFIG_FIXED] = $this->__StdClassConfig_isFixed;
}
if (static::class !== self::class) {
$data['class'] = static::class;
}
return $data;
}
/**
* @param array|\stdClass $serialized
*
* @return StdClassConfig
* @noinspection MagicMethodsValidityInspection
* @throws InvalidConfigTypeException
*/
public function __unserialize($serialized)
{
$this->validateSerializedInput($serialized);
$class = $serialized->class ?? static::class;
unset($serialized->class);
// check if different class is required!
if ($class !== static::class) {
try {
$class = new ReflectionClass($class);
$config = $class->newInstanceWithoutConstructor();
return $config->__unserialize($serialized);
} catch (ReflectionException $e) {
}
}
$parentFixed = $serialized->{self::SERIALIZE_VALUE__PARENT_FIXED} ?? false;
unset($serialized->{self::SERIALIZE_VALUE__PARENT_FIXED});
$configFixed = $serialized->{self::SERIALIZE_VALUE__CONFIG_FIXED} ?? false;
unset($serialized->{self::SERIALIZE_VALUE__CONFIG_FIXED});
$config = $this;
parent::__unserialize($serialized);
$this->__StdClassConfigurable_isFixed = $parentFixed;
$this->__StdClassConfig_isFixed = $configFixed;
return $config;
}
public function clear(): self
{
if ($this->isLoading()) {
return $this;
}
parent::clear();
$this->default = null;
return $this;
}
public function isLoading(): bool
{
return $this->__StdClassConfigurable_loading;
}
public function setLoading(bool $loading): self
{
$this->__StdClassConfigurable_loading = $loading;
return $this;
}
public function isFixed(): bool
{
return $this->__StdClassConfigurable_isFixed && !$this->__StdClassConfigurable_loading;
}
/**
* Once called, no (further) dynamic properties can be added
*
* @see static::$__StdClassConfig_isFixed
* @noinspection PhpUnused
*/
public function fixate(bool $fixed): self
{
$this->__StdClassConfigurable_isFixed = $this->__StdClassConfig_isFixed || $fixed;
return $this;
}
/**
* @return bool
* @noinspection PhpUnused
*/
public function isConfigFixed(): bool
{
return $this->__StdClassConfig_isFixed;
}
/**
* Once called, no (further) dynamic properties can be added
*
* @see static::$__StdClassConfig_isFixed
* @noinspection PhpUnused
*/
public function fixateConfig(bool $fixed): self
{
$this->__StdClassConfig_isFixed = $this->__StdClassConfig_isFixed || $fixed;
return $this;
}
public function isModified(): bool
{
return $this->__StdClassConfigurable_isFixed
|| $this->__StdClassConfig_isFixed
|| parent::isModified();
}
public function getReflection(): ReflectionObject
{
return new ReflectionObject($this->__StdClassConfig_parent->get() ?? new \stdClass());
}
/**
* @throws InvalidConfigTypeException
*/
public static function &getConfig(): self
{
$trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 3);
$i = 1;
$destroy = $trace[$i]['class'] === static::class && $trace[$i]['function'] === 'destroyConfig';
if ($destroy) {
++$i;
}
$parent = $trace[$i]['object'] ?? null;
if (!$parent instanceof StdClassConfigurable) {
throw new RuntimeException(sprintf('Method %s can only be called from a %s instance itself', __METHOD__,
StdClassConfigurable::class));
}
try {
$config = self::$__StdClassConfig_config[$parent];
} catch (UnexpectedValueException $e) {
$config = null;
} catch (Error $e) {
if ($e->getMessage() === 'Typed static property ' . self::class . '::$__StdClassConfig_config must not be accessed before initialization') {
self::$__StdClassConfig_config = new SplObjectStorage();
$config = null;
}
}
if ($destroy) {
if ($config) {
unset(self::$__StdClassConfig_config[$parent]);
}
} elseif ($config === null) {
self::$__StdClassConfig_config[$parent] = $config = new static($parent);
}
return $config;
}
/**
* @throws Throwable
* @internal
*/
public static function destroyConfig(): void
{
static::getConfig();
}
/**
* @return mixed
*/
public function getParent(): ?StdClassConfigurable
{
return $this->__StdClassConfig_parent->get();
}
/**
* @throws InvalidConfigTypeException
*/
protected function validateSerializedInput(&$serialized, ?array $requiredFields = self::UNSERIALIZE_REQUIRED_VALUES, bool $throw = true): ?bool
{
// this is used to identify already-validated data
self::$validatedObject ??= self::validatedObject();
if ($serialized instanceof self::$validatedObject) {
return true;
}
$valid = parent::validateSerializedInput($serialized, $requiredFields, $throw);
if ($valid === null) {
return null;
}
$class = $serialized->{self::SERIALIZE_VALUE__CLASS} ?? null;
if ($class === null || $this->validateClassInheritance($class, $throw)) {
return $valid;
}
return null;
}
}

View File

@ -0,0 +1,296 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\libs;
use humhub\exceptions\InvalidArgumentTypeException;
use humhub\exceptions\InvalidConfigTypeException;
use Throwable;
use yii\base\UnknownPropertyException;
/**
* WARNING: This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
* ---
*
* Extending from \humhub\libs\StdClass, this class some additional features:
* - a defaultValue can be set which will be used in case a non-existent property is read: \
* `static::setDefaultValue($value)`
* `static::getDefaultValue()`
* - the object provides a static::config() accessor to a separate property namespace
* - allows the object to be fixated, so that now additional property can be added \
* with `static::fixate()`
* (this is not write-protection, just disallowing new properties!)
*
* @since 1.15 This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
* @internal (ToDo)
* @see StdClassConfig
*/
class StdClassConfigurable extends StdClass
{
public const SERIALIZE_FORMAT = 1;
protected const SERIALIZE_VALUE__CLASS = 'class';
protected const SERIALIZE_VALUE__CONFIG = 'config';
protected const UNSERIALIZE_REQUIRED_VALUES = [
self::SERIALIZE_VALUE__DATA,
self::SERIALIZE_VALUE__CLASS,
self::SERIALIZE_VALUE__CONFIG,
];
/**
* @inerhitdoc
* @noinspection MagicMethodsValidityInspection
*/
public function __construct(...$args)
{
$config = $this->config();
parent::__construct(...$args);
$config->setLoading(false);
return $this;
}
public function __destruct()
{
try {
StdClassConfig::destroyConfig();
} catch (Throwable $e) {
}
}
/**
* @throws UnknownPropertyException
*/
public function __get($name)
{
if ($name === null) {
return $this->getDefaultValue();
}
if ($this->config()->isFixed()) {
throw new UnknownPropertyException('Getting unknown property: ' . static::class . '::$' . $name);
}
return parent::__get($name);
}
/**
* @throws UnknownPropertyException
*/
public function __set($name, $value)
{
if ($name === null) {
return $this->setDefaultValue($value);
}
if ($this->config()->isFixed()) {
throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::$' . $name);
}
return parent::__set($name, $value);
}
public function __isset($name)
{
if ($name === null) {
return $this->hasDefaultValue();
}
return parent::__isset($name);
}
public function __unset($name)
{
if ($name === null) {
$this->setDefaultValue(null);
return $this;
}
return parent::__unset($name);
}
public function __serialize(): array
{
$isSerializing = self::isSerializing($this);
$data = parent::__serialize();
/**
* Loop through the data and remove any unmodified instance of `StdClass`.
*
* (Use $item as a reference so that arrays don't get copied.)
*
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
*/
foreach ($data[self::SERIALIZE_VALUE__DATA] ?? [] as $key => &$item) {
if ($item instanceof StdClass && !$item->isModified()) {
unset($data[self::SERIALIZE_VALUE__DATA][$key]);
}
}
unset($item);
if ($isSerializing && static::class !== self::class) {
$data[self::SERIALIZE_VALUE__CLASS] = static::class;
}
$data[self::SERIALIZE_VALUE__CONFIG] = (object)$this->config()->__serialize();
self::isSerializing($this, !$isSerializing);
return $data;
}
/**
* @param array|\stdClass $serialized
*
* @return self
* @throws InvalidArgumentTypeException|InvalidConfigTypeException
* @noinspection MagicMethodsValidityInspection
*/
public function __unserialize($serialized)
{
/**
* $serialized may be a \stdClass object created by `static::unserialize()`
*
* @see static::unserialize()
*/
$this->validateSerializedInput($serialized);
// clear only after validation was successful
$this->clear();
$config = &$serialized->{self::SERIALIZE_VALUE__CONFIG};
unset($serialized->{self::SERIALIZE_VALUE__CONFIG});
$return = parent::__unserialize($serialized);
$return->config()
->__unserialize($config)
->setLoading(false);
return $return;
}
protected function &config(): StdClassConfig
{
return StdClassConfig::getConfig();
}
public function clear(): self
{
$config = $this->config();
if ($config->isLoading()) {
return $this;
}
$config->clear();
return parent::clear();
}
public function isModified(): bool
{
return parent::isModified() || $this->config()->isModified();
}
public function isFixed(): bool
{
return $this->config()->isFixed();
}
/**
* Once called, no (further) dynamic properties can be added
*
* @see StdClassConfig::fixate()
* @noinspection PhpUnused
*/
public function fixate(): self
{
$this->config()->fixate(true);
return $this;
}
/**
* @return mixed|null
*/
public function getDefaultValue()
{
return $this->config()->default;
}
/**
* @param $value
*
* @return static
*/
public function setDefaultValue($value): self
{
$this->config()->default = $value;
return $this;
}
public function hasDefaultValue(): bool
{
return $this->config()->default !== null;
}
/**
* This function is used internally to validate property names
*
* @param $name
* @param string $method
* @param string|array $parameter
*
* @return void
*/
protected function validatePropertyName($name, string $method, string $parameter = '$name'): ?string
{
if ($name === null) {
return null;
}
return parent::validatePropertyName($name, $method, $parameter);
}
/**
* @throws InvalidConfigTypeException
*/
protected function validateSerializedInput(&$serialized, ?array $requiredFields = self::UNSERIALIZE_REQUIRED_VALUES, bool $throw = true): ?bool
{
// this is used to identify already-validated data
self::$validatedObject ??= self::validatedObject();
if ($serialized instanceof self::$validatedObject) {
return true;
}
$valid = parent::validateSerializedInput($serialized, $requiredFields, $throw);
if ($valid === null) {
return null;
}
$class = $serialized->{self::SERIALIZE_VALUE__CLASS} ?? null;
if ($class !== null && !$this->validateClassInheritance($class, $throw)) {
return null;
}
$config = &$serialized->{self::SERIALIZE_VALUE__CONFIG};
$this->validateClassIncomplete($config);
return $valid;
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\file\libs;
use humhub\libs\StdClassConfigurable;
/**
* WARNING: This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
* ---
*
* This class is used to access the data in the `File::$metadata` field.
*
* @since 1.15; this class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
* @internal (ToDo)
*/
class Metadata extends StdClassConfigurable
{
}

View File

@ -0,0 +1,39 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\components\Migration;
use humhub\modules\file\models\File;
/**
* Add and film GUID column
*/
class m230618_135510_file_add_metadata_column extends Migration
{
// protected properties
protected string $table;
public function __construct($config = [])
{
$this->table = File::tableName();
parent::__construct($config);
}
/**
* {@inheritdoc}
*/
public function safeUp(): void
{
$this->safeAddColumn(
$this->table,
'metadata',
$this->string(4000)
->after('size')
);
}
}

View File

@ -11,10 +11,12 @@ namespace humhub\modules\file\models;
use humhub\components\ActiveRecord;
use humhub\components\behaviors\GUID;
use humhub\components\behaviors\PolymorphicRelation;
use humhub\libs\StdClass;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\components\ContentAddonActiveRecord;
use humhub\modules\file\components\StorageManager;
use humhub\modules\file\components\StorageManagerInterface;
use humhub\modules\file\libs\Metadata;
use humhub\modules\user\models\User;
use Throwable;
use Yii;
@ -40,6 +42,26 @@ use yii\web\UploadedFile;
* @property string $title
* @property string $mime_type
* @property string $size
* @property-read Metadata $metadata since 1.15. Note, $metadata is still experimental. Expect changes in v1.16 (ToDo).
* This property is read-only in the sense that no new instance be assigned to the model.
* Edit data always by working on the object itself.
* You best retrieve is using `static::getMetadata()`.
* E.g, to set a value you could do:
* ```
* // setting a single value
* $model->getMetadata()->property1 = "some value";
* // or
* $model->getMetadata()['property2'] = "some other value";
*
* // setting multiple values
* $metadata = $model->getMetadata();
* $metadata->property1 = "some value";
* $metadata['property2'] = "some other value";
*
* // alternatively, the `Metadata::addValues()` method can be used:
* $model->getMetadata()->addValues(['property1' => "some value", 'property2' => "some other value"] = "some other value";
* ```
*
* @property string|null $object_model
* @property integer|null $object_id
* @property integer|null $content_id
@ -152,6 +174,15 @@ class File extends FileCompat
];
}
public function __get($name)
{
if ($name === 'metadata') {
return $this->getMetadata();
}
return parent::__get($name);
}
/**
* Gets a query for [[FileHistory]].
*
@ -180,6 +211,12 @@ class File extends FileCompat
$this->sort_order ??= 0;
$metadata = $this->getAttribute('metadata');
if (($metadata instanceof StdClass) && !$metadata->isModified()) {
$this->setAttribute('metadata', null);
}
return parent::beforeSave($insert);
}
@ -316,6 +353,40 @@ class File extends FileCompat
return $this->object_model === get_class($record) && $this->object_id == $record->getPrimaryKey();
}
/**
* @return Metadata
*/
public function getMetadata(): Metadata
{
/** @var Metadata|null $metadata */
$metadata = $this->getAttribute('metadata');
if ($metadata instanceof Metadata) {
return $metadata;
}
$metadata = new Metadata($metadata);
$this->setAttribute('metadata', $metadata);
return $metadata;
}
/**
* @param string|array $metadata
*
* @return File
*/
public function setMetadata($metadata): File
{
/** @var Metadata|null $md */
$md = $this->metadata;
$md->addValues($metadata);
return $this;
}
/**
* Returns the StorageManager
*
@ -379,15 +450,15 @@ class File extends FileCompat
$store = $this->getStore();
if ($file instanceof UploadedFile) {
$this->getStore()->set($file);
} elseif ($file instanceof File) {
$store->set($file);
} elseif ($file instanceof self) {
if ($file->isAssigned()) {
throw new InvalidArgumentException('Already assigned File records cannot stored as another File record.');
}
$this->getStore()->setByPath($file->getStore()->get());
$store->setByPath($file->getStore()->get());
$file->delete();
} elseif (is_string($file) && is_file($file)) {
$this->getStore()->setByPath($file);
$store->setByPath($file);
}
$this->afterNewStoredFile();

View File

@ -0,0 +1,52 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2019-2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace tests\codeception\unit\modules\file;
use humhub\modules\file\libs\Metadata;
use tests\codeception\_support\HumHubDbTestCase;
class MetadataTest extends HumHubDbTestCase
{
private static $useData = false;
public function _fixtures(): array
{
return self::$useData
? parent::_fixtures()
: [];
}
public function testInstantiationStdClass()
{
$serialized = 'O:33:"humhub\modules\file\libs\Metadata":2:{s:1:"v";i:1;s:6:"config";O:8:"stdClass":1:{s:1:"v";i:1;}}';
$instance = new Metadata();
static::assertInstanceOf(Metadata::class, $instance);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
static::assertEquals($serialized, $instance->serialize());
$serialized = 'O:33:"humhub\modules\file\libs\Metadata":3:{s:1:"v";i:1;s:2:"_0";a:1:{s:3:"foo";s:3:"bar";}s:6:"config";O:8:"stdClass":1:{s:1:"v";i:1;}}';
$instance = new Metadata(['foo' => 'bar']);
static::assertInstanceOf(Metadata::class, $instance);
static::assertCount(1, $instance);
static::assertEquals(['foo' => 'foo'], $instance->fields());
static::assertEquals(['foo' => 'bar'], $instance->toArray());
static::assertEquals($serialized, $instance->serialize());
$instance = new Metadata($serialized);
static::assertInstanceOf(Metadata::class, $instance);
static::assertCount(1, $instance);
static::assertEquals(['foo' => 'foo'], $instance->fields());
static::assertEquals(['foo' => 'bar'], $instance->toArray());
static::assertEquals($serialized, $instance->serialize());
}
}

View File

@ -0,0 +1,164 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2018-2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\tests\codeception\unit;
use Codeception\Test\Unit;
use humhub\exceptions\InvalidArgumentTypeException;
use humhub\exceptions\InvalidArgumentValueException;
use humhub\libs\StdClass;
use humhub\libs\StdClassConfigurable;
/**
* Class MimeHelperTest
*/
class StdClassTest extends Unit
{
public function testInstantiationStdClass()
{
$serialized = 'O:20:"humhub\libs\StdClass":1:{s:1:"v";i:1;}';
$instance = new StdClass();
static::assertInstanceOf(StdClass::class, $instance);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
static::assertEquals($serialized, $instance->serialize());
static::assertEquals($serialized, serialize($instance));
$instance = new StdClass([]);
static::assertInstanceOf(StdClass::class, $instance);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
static::assertEquals($serialized, $instance->serialize());
$instance = new StdClass(null);
static::assertInstanceOf(StdClass::class, $instance);
static::assertCount(0, $instance);
$instance = new StdClass($serialized);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
static::assertEquals($serialized, $instance->serialize());
$instance = new StdClass('O:20:"humhub\libs\StdClass":2:{s:1:"v";i:1;i:0;a:0:{}}');
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
static::assertEquals($serialized, $instance->serialize());
$serialized = 'O:20:"humhub\libs\StdClass":2:{s:1:"v";i:1;s:2:"_0";a:1:{s:3:"foo";s:3:"bar";}}';
$instance = new StdClass(['foo' => 'bar']);
static::assertInstanceOf(StdClass::class, $instance);
static::assertCount(1, $instance);
static::assertEquals(['foo' => 'foo'], $instance->fields());
static::assertEquals(['foo' => 'bar'], $instance->toArray());
static::assertEquals($serialized, $instance->serialize());
$instance = new StdClass($serialized);
static::assertCount(1, $instance);
static::assertEquals(['foo' => 'foo'], $instance->fields());
static::assertEquals(['foo' => 'bar'], $instance->toArray());
static::assertEquals($serialized, $instance->serialize());
$this->expectException(InvalidArgumentValueException::class);
$this->expectExceptionMessage('Argument $serialized passed to humhub\libs\StdClass::unserialize must be string starting with \'O:20:"humhub\libs\StdClass"\' - O:20:"some\dangerous\class":1:{s:4:"data";a:1:{s:3:"foo";s:3:"bar";}} given.');
new StdClass('O:20:"some\\dangerous\\class":1:{s:4:"data";a:1:{s:3:"foo";s:3:"bar";}}');
}
public function testInvalidInstantiationStdClassWithTrue()
{
$this->expectException(InvalidArgumentTypeException::class);
$this->expectExceptionMessage('Argument ...$args passed to humhub\libs\StdClass::addValues must be one of the following types: array, Traversable - bool given.');
new StdClass(false);
}
public function testInvalidInstantiationStdClassWithFalse()
{
$this->expectException(InvalidArgumentTypeException::class);
$this->expectExceptionMessage('Argument ...$args passed to humhub\libs\StdClass::addValues must be one of the following types: array, Traversable - bool given.');
new StdClass(false);
}
public function testInstantiationStdClassConfigurable()
{
$serialized = 'O:32:"humhub\libs\StdClassConfigurable":2:{s:1:"v";i:1;s:6:"config";O:8:"stdClass":1:{s:1:"v";i:1;}}';
$instance = new StdClassConfigurable();
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
static::assertEquals($serialized, $instance->serialize());
// try again with the now initialized config storage
$instance = new StdClassConfigurable();
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(0, $instance);
// try with different parameter values
$instance = new StdClassConfigurable([]);
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
$instance = new StdClassConfigurable(null);
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
$instance = new StdClassConfigurable($serialized);
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
static::assertEquals($serialized, $instance->serialize());
$instance = new StdClassConfigurable('O:32:"humhub\libs\StdClassConfigurable":3:{s:1:"v";i:1;i:0;a:0:{}s:6:"config";O:8:"stdClass":5:{s:1:"v";i:1;s:2:"_0";a:1:{s:7:"default";N;}s:2:"_1";b:0;s:2:"_2";b:0;s:2:"_3";b:0;}}');
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(0, $instance);
static::assertEquals([], $instance->fields());
static::assertEquals($serialized, $instance->serialize());
$serialized = 'O:32:"humhub\libs\StdClassConfigurable":3:{s:1:"v";i:1;s:2:"_0";a:1:{s:3:"foo";s:3:"bar";}s:6:"config";O:8:"stdClass":1:{s:1:"v";i:1;}}';
$instance = new StdClassConfigurable(['foo' => 'bar']);
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(1, $instance);
static::assertEquals(['foo' => 'foo'], $instance->fields());
static::assertEquals(['foo' => 'bar'], $instance->toArray());
static::assertEquals($serialized, $instance->serialize());
$instance = new StdClassConfigurable($serialized);
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(1, $instance);
static::assertEquals(['foo' => 'foo'], $instance->fields());
static::assertEquals(['foo' => 'bar'], $instance->toArray());
static::assertFalse($instance->isFixed());
static::assertEquals($serialized, $instance->serialize());
$serialized = 'O:32:"humhub\libs\StdClassConfigurable":3:{s:1:"v";i:1;s:2:"_0";a:1:{s:3:"foo";s:3:"bar";}s:6:"config";O:8:"stdClass":2:{s:1:"v";i:1;s:2:"_1";b:1;}}';
$instance = StdClassConfigurable::create(['foo' => 'bar'])->fixate();
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(1, $instance);
static::assertEquals(['foo' => 'foo'], $instance->fields());
static::assertEquals(['foo' => 'bar'], $instance->toArray());
static::assertTrue($instance->isFixed(), 'StdClassConfigurable object is not fixated!');
static::assertEquals($serialized, $instance->serialize());
$instance = new StdClassConfigurable($serialized);
static::assertInstanceOf(StdClassConfigurable::class, $instance);
static::assertCount(1, $instance);
static::assertEquals(['foo' => 'foo'], $instance->fields());
static::assertTrue($instance->isFixed(), 'StdClassConfigurable object is not fixated!');
static::assertEquals($serialized, $instance->serialize());
}
}