getDescriptor()); return implode(Delimiter::ELEMENT, static::flattenAndTrimEnd($serializedElements)) . Delimiter::SEGMENT; } /** * @param BaseSegment|BaseDeg|null $obj An object to be serialized. If null, all fields are implicitly null. * @param BaseDescriptor $descriptor The descriptor for the object to be serialized. * @return array A partial serialization of that object, namely a (possibly nested) array with all of its elements * serialized independently, and at the right indices. In order to put subsequent elements in the right * position, the returned array may contain emtpy strings as gaps/buffers in the middle (for subsequent elements * in $obj) and/or at the end (for subsequent elements added by the caller for data following $obj). */ private static function serializeElements($obj, BaseDescriptor $descriptor): array { $isSegment = $descriptor instanceof SegmentDescriptor; $serializedElements = []; $lastKey = array_key_last($descriptor->elements); for ($index = 0; $index <= $lastKey; ++$index) { if (!array_key_exists($index, $descriptor->elements)) { $serializedElements[$index] = ''; continue; } $elementDescriptor = $descriptor->elements[$index]; $value = $obj === null ? null : $obj->{$elementDescriptor->field}; if (array_key_exists($index, $serializedElements)) { throw new \AssertionError("Duplicate index $index"); } if ($elementDescriptor->repeated === 0) { $serializedElements[$index] = static::serializeElement($value, $elementDescriptor->type, $isSegment); } else { if ($value !== null && !is_array($value)) { throw new \InvalidArgumentException( "Expected array value for $descriptor->class.$elementDescriptor->field, got: $value"); } for ($repetition = 0; $repetition < $elementDescriptor->repeated; ++$repetition) { $serializedElements[$index + $repetition] = static::serializeElement( $value === null || $repetition >= count($value) ? null : $value[$repetition], $elementDescriptor->type, $isSegment); } } } return $serializedElements; } /** * @param mixed|null $value The value to be serialized. * @param string|\ReflectionClass $type The type of the value. * @param bool $fullySerialize If true, the result is always a string, complex values are imploded as a DEG. * @return string|array The serialized value. In case $type is a complex type and $fullySerialize is false, this * returns a (possibly nested) array of strings. */ private static function serializeElement($value, $type, bool $fullySerialize) { if (is_string($type)) { return static::serializeDataElement($value, $type); } elseif ($type->getName() === Bin::class) { /* @var Bin|null $value */ return $value === null ? '' : $value->toString(); } elseif ($fullySerialize) { return static::serializeDeg($value, DegDescriptor::get($type->name)); } else { return static::serializeElements($value, DegDescriptor::get($type->name)); } } /** * @param array $elements A possibly nested array of string values. * @return string[] A flat array with the same string values (using in-order tree traversal), but empty values * removed from the end. */ private static function flattenAndTrimEnd(array $elements): array { $result = []; $nonemptyLength = 0; foreach (new \RecursiveIteratorIterator(new \RecursiveArrayIterator($elements)) as $element) { $result[] = $element; if ($element !== '') { $nonemptyLength = count($result); } } return array_slice($result, 0, $nonemptyLength); } }