format('Y-m-d') : $data->format(self::$dateTimeFormat); } if ($data instanceof \BackedEnum) { return $data->value; } if (is_array($data)) { foreach ($data as $property => $value) { $data[$property] = self::sanitizeForSerialization($value); } return $data; } if (is_object($data)) { $values = []; if ($data instanceof ModelInterface) { $formats = $data::openAPIFormats(); foreach ($data::openAPITypes() as $property => $openAPIType) { $getter = $data::getters()[$property]; $value = $data->$getter(); if ($value !== null && !in_array($openAPIType, ['\DateTime', '\SplFileObject', 'array', 'bool', 'boolean', 'byte', 'float', 'int', 'integer', 'mixed', 'number', 'object', 'string', 'void'], true)) { if (is_subclass_of($openAPIType, '\BackedEnum')) { if (is_scalar($value)) { $value = $openAPIType::tryFrom($value); if ($value === null) { $imploded = implode("', '", array_map(fn($case) => $case->value, $openAPIType::cases())); throw new \InvalidArgumentException( sprintf( "Invalid value for enum '%s', must be one of: '%s'", $openAPIType::class, $imploded ) ); } } } } if (($data::isNullable($property) && $data->isNullableSetToNull($property)) || $value !== null) { $values[$data::attributeMap()[$property]] = self::sanitizeForSerialization($value, $openAPIType, $formats[$property]); } } } else { foreach($data as $property => $value) { $values[$property] = self::sanitizeForSerialization($value); } } return (object)$values; } else { return (string)$data; } } /** * Sanitize filename by removing path. * e.g. ../../sun.gif becomes sun.gif * * @param string $filename filename to be sanitized * * @return string the sanitized filename */ public static function sanitizeFilename(string $filename): string { if (preg_match("/.*[\/\\\\](.*)$/", $filename, $match)) { return $match[1]; } else { return $filename; } } /** * Shorter timestamp microseconds to 6 digits length. * * @param string $timestamp Original timestamp * * @return string the shorten timestamp */ public static function sanitizeTimestamp(string $timestamp): string { return preg_replace('/(:\d{2}.\d{6})\d*/', '$1', $timestamp); } /** * Take value and turn it into a string suitable for inclusion in * the path, by url-encoding. * * @param string $value a string which will be part of the path * * @return string the serialized object */ public static function toPathValue(string $value): string { return rawurlencode(self::toString($value)); } /** * Checks if a value is empty, based on its OpenAPI type. * * @param mixed $value * @param string $openApiType * * @return bool true if $value is empty */ private static function isEmptyValue(mixed $value, string $openApiType): bool { # If empty() returns false, it is not empty regardless of its type. if (!empty($value)) { return false; } # Null is always empty, as we cannot send a real "null" value in a query parameter. if ($value === null) { return true; } return match ($openApiType) { # For numeric values, false and '' are considered empty. # This comparison is safe for floating point values, since the previous call to empty() will # filter out values that don't match 0. 'int','integer' => $value !== 0, 'number'|'float' => $value !== 0 && $value !== 0.0, # For boolean values, '' is considered empty 'bool','boolean' => !in_array($value, [false, 0], true), # For string values, '' is considered empty. 'string' => $value === '', # For all the other types, any value at this point can be considered empty. default => true }; } /** * Take query parameter properties and turn it into an array suitable for * native http_build_query or GuzzleHttp\Psr7\Query::build. * * @param mixed $value Parameter value * @param string $paramName Parameter name * @param string $openApiType OpenAPIType e.g. array or object * @param string $style Parameter serialization style * @param bool $explode Parameter explode option * @param bool $required Whether query param is required or not * * @return array */ public static function toQueryValue( mixed $value, string $paramName, string $openApiType = 'string', string $style = 'form', bool $explode = true, bool $required = true ): array { # Check if we should omit this parameter from the query. This should only happen when: # - Parameter is NOT required; AND # - its value is set to a value that is equivalent to "empty", depending on its OpenAPI type. For # example, 0 as "int" or "boolean" is NOT an empty value. if (self::isEmptyValue($value, $openApiType)) { if ($required) { return ["{$paramName}" => '']; } else { return []; } } # Handle DateTime objects in query if($openApiType === "\DateTime" && $value instanceof DateTime) { return ["{$paramName}" => $value->format(self::$dateTimeFormat)]; } $query = []; $value = (in_array($openApiType, ['object', 'array'], true)) ? (array)$value : $value; // since \GuzzleHttp\Psr7\Query::build fails with nested arrays // need to flatten array first $flattenArray = function ($arr, $name, &$result = []) use (&$flattenArray, $style, $explode) { if (!is_array($arr)) return $arr; foreach ($arr as $k => $v) { $prop = ($style === 'deepObject') ? "{$name}[{$k}]" : $k; if (is_array($v)) { $flattenArray($v, $prop, $result); } else { if ($style !== 'deepObject' && !$explode) { // push key itself $result[] = $prop; } $result[$prop] = $v; } } return $result; }; $value = $flattenArray($value, $paramName); // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values if ($openApiType === 'array' && $style === 'deepObject' && $explode) { return $value; } if ($openApiType === 'object' && ($style === 'deepObject' || $explode)) { return $value; } if ('boolean' === $openApiType && is_bool($value)) { $value = self::convertBoolToQueryStringFormat($value); } // handle style in serializeCollection $query[$paramName] = ($explode) ? $value : self::serializeCollection((array)$value, $style); return $query; } /** * Convert boolean value to format for query string. * * @param bool $value Boolean value * * @return int|string Boolean value in format */ public static function convertBoolToQueryStringFormat(bool $value): int|string { if (Configuration::BOOLEAN_FORMAT_STRING == Configuration::getDefaultConfiguration()->getBooleanFormatForQueryString()) { return $value ? 'true' : 'false'; } return (int) $value; } /** * Take value and turn it into a string suitable for inclusion in * the header. If it's a string, pass through unchanged * If it's a datetime object, format it in ISO8601 * * @param string $value a string which will be part of the header * * @return string the header string */ public static function toHeaderValue(string $value): string { $callable = [$value, 'toHeaderValue']; if (is_callable($callable)) { return $callable(); } return self::toString($value); } /** * Take value and turn it into an array suitable for inclusion in * the http body (form parameter). If it's a string, pass through unchanged * If it's a datetime object, format it in ISO8601 * * @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter * * @return array [key => value] of formdata */ public static function toFormValue( string $key, string|bool|array|DateTime|ArrayAccess|\SplFileObject $value, ): array { if ($value instanceof \SplFileObject) { return [$key => $value->getRealPath()]; } elseif (is_array($value) || $value instanceof ArrayAccess) { $flattened = []; $result = []; self::flattenArray(json_decode(json_encode($value), true), $flattened); foreach ($flattened as $k => $v) { $result["{$key}{$k}"] = self::toString($v); } return $result; } else { return [$key => self::toString($value)]; } } /** * Take value and turn it into a string suitable for inclusion in * the parameter. If it's a string, pass through unchanged * If it's a datetime object, format it in ISO8601 * If it's a boolean, convert it to "true" or "false". * * @param string|bool|DateTime $value the value of the parameter * * @return string the header string */ public static function toString(string|bool|DateTime $value): string { if ($value instanceof DateTime) { // datetime in ISO8601 format return $value->format(self::$dateTimeFormat); } elseif (is_bool($value)) { return $value ? 'true' : 'false'; } else { return (string) $value; } } /** * Serialize an array to a string. * * @param array $collection collection to serialize to a string * @param string $style the format use for serialization (csv, * ssv, tsv, pipes, multi) * @param bool $allowCollectionFormatMulti allow collection format to be a multidimensional array * * @return string */ public static function serializeCollection(array $collection, string $style, bool $allowCollectionFormatMulti = false): string { if ($allowCollectionFormatMulti && ('multi' === $style)) { // http_build_query() almost does the job for us. We just // need to fix the result of multidimensional arrays. return preg_replace('/%5B[0-9]+%5D=/', '=', http_build_query($collection, '', '&')); } return match ($style) { 'pipeDelimited', 'pipes' => implode('|', $collection), 'tsv' => implode("\t", $collection), 'spaceDelimited', 'ssv' => implode(' ', $collection), default => implode(',', $collection), }; } /** * Deserialize a JSON string into an object * * @param mixed $data object or primitive to be deserialized * @param string $class class name is passed as a string * @param string[]|null $httpHeaders HTTP headers * * @return mixed a single or an array of $class instances */ public static function deserialize(mixed $data, string $class, ?array $httpHeaders = null): mixed { if (null === $data) { return null; } if (strcasecmp(substr($class, -2), '[]') === 0) { $data = is_string($data) ? json_decode($data) : $data; if (!is_array($data)) { throw new \InvalidArgumentException("Invalid array '$class'"); } $subClass = substr($class, 0, -2); $values = []; foreach ($data as $key => $value) { $values[] = self::deserialize($value, $subClass, null); } return $values; } if (preg_match('/^(array<|map\[)/', $class)) { // for associative array e.g. array $data = is_string($data) ? json_decode($data) : $data; settype($data, 'array'); $inner = substr($class, 4, -1); $deserialized = []; if (strrpos($inner, ",") !== false) { $subClass_array = explode(',', $inner, 2); $subClass = $subClass_array[1]; foreach ($data as $key => $value) { $deserialized[$key] = self::deserialize($value, $subClass, null); } } return $deserialized; } if ($class === 'mixed') { settype($data, gettype($data)); return $data; } if ($class === '\DateTime') { // Some APIs return an invalid, empty string as a // date-time property. DateTime::__construct() will return // the current time for empty input which is probably not // what is meant. The invalid empty string is probably to // be interpreted as a missing field/value. Let's handle // this graceful. if (!empty($data)) { try { return new DateTime($data); } catch (\Exception $exception) { // Some APIs return a date-time with too high nanosecond // precision for php's DateTime to handle. // With provided regexp 6 digits of microseconds saved return new DateTime(self::sanitizeTimestamp($data)); } } else { return null; } } if ($class === '\Psr\Http\Message\StreamInterface') { return Utils::streamFor($data); } if ($class === '\SplFileObject') { $data = Utils::streamFor($data); /** @var \Psr\Http\Message\StreamInterface $data */ // determine file name if ( is_array($httpHeaders) && array_key_exists('Content-Disposition', $httpHeaders) && preg_match('/inline; filename=[\'"]?([^\'"\s]+)[\'"]?$/i', $httpHeaders['Content-Disposition'], $match) ) { $filename = Configuration::getDefaultConfiguration()->getTempFolderPath() . DIRECTORY_SEPARATOR . self::sanitizeFilename($match[1]); } else { $filename = tempnam(Configuration::getDefaultConfiguration()->getTempFolderPath(), ''); } $file = fopen($filename, 'w'); while ($chunk = $data->read(200)) { fwrite($file, $chunk); } fclose($file); return new \SplFileObject($filename, 'r'); } /** @psalm-suppress ParadoxicalCondition */ // handle primitive types if (in_array($class, ['\DateTime', '\SplFileObject'], true)) { return $data; } elseif (in_array($class, ['array', 'bool', 'boolean', 'float', 'double', 'int', 'integer', 'object', 'string', 'null'], true)) { // type ref: https://www.php.net/manual/en/function.settype.php // byte, mixed, void in the old php client were removed settype($data, $class); return $data; } if (is_subclass_of($class, '\BackedEnum')) { $data = $class::tryFrom($data); if ($data === null) { $imploded = implode("', '", array_map(fn($case) => $case->value, $class::cases())); throw new \InvalidArgumentException("Invalid value for enum '$class', must be one of: '$imploded'"); } return $data; } else { $data = is_string($data) ? json_decode($data) : $data; if (is_array($data)) { $data = (object)$data; } // If a discriminator is defined and points to a valid subclass, use it. $discriminator = $class::DISCRIMINATOR; if (!empty($discriminator) && isset($data->{$discriminator}) && is_string($data->{$discriminator})) { $subclass = '\OpenAPI\Client\Model\\' . $data->{$discriminator}; if (is_subclass_of($subclass, $class)) { $class = $subclass; } } /** @var ModelInterface $instance */ $instance = new $class(); foreach ($instance::openAPITypes() as $property => $type) { $propertySetter = $instance::setters()[$property]; if (!isset($propertySetter)) { continue; } if (!isset($data->{$instance::attributeMap()[$property]})) { if ($instance::isNullable($property)) { $instance->$propertySetter(null); } continue; } if (isset($data->{$instance::attributeMap()[$property]})) { $propertyValue = $data->{$instance::attributeMap()[$property]}; $instance->$propertySetter(self::deserialize($propertyValue, $type, null)); } } return $instance; } } /** * Build a query string from an array of key value pairs. * * This function can use the return value of `parse()` to build a query * string. This function does not modify the provided keys when an array is * encountered (like `http_build_query()` would). * * @param array $params Query string parameters. * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 * to encode using RFC3986, or PHP_QUERY_RFC1738 * to encode using RFC1738. */ public static function buildQuery(array $params, $encoding = PHP_QUERY_RFC3986): string { if (!$params) { return ''; } if ($encoding === false) { $encoder = function (string $str): string { return $str; }; } elseif ($encoding === PHP_QUERY_RFC3986) { $encoder = 'rawurlencode'; } elseif ($encoding === PHP_QUERY_RFC1738) { $encoder = 'urlencode'; } else { throw new \InvalidArgumentException('Invalid type'); } $castBool = Configuration::BOOLEAN_FORMAT_INT == Configuration::getDefaultConfiguration()->getBooleanFormatForQueryString() ? function ($v) { return (int) $v; } : function ($v) { return $v ? 'true' : 'false'; }; $qs = ''; foreach ($params as $k => $v) { $k = $encoder((string) $k); if (!is_array($v)) { $qs .= $k; $v = is_bool($v) ? $castBool($v) : $v; if ($v !== null) { $qs .= '='.$encoder((string) $v); } $qs .= '&'; } else { foreach ($v as $vv) { $qs .= $k; $vv = is_bool($vv) ? $castBool($vv) : $vv; if ($vv !== null) { $qs .= '='.$encoder((string) $vv); } $qs .= '&'; } } } return $qs ? (string) substr($qs, 0, -1) : ''; } /** * Flattens an array of Model object and generates an array compatible * with formdata - a single-level array where the keys use bracket * notation to signify nested data. * * credit: https://github.com/FranBar1966/FlatPHP */ private static function flattenArray( ArrayAccess|array $source, array &$destination, string $start = '', ) { $opt = [ 'prefix' => '[', 'suffix' => ']', 'suffix-end' => true, 'prefix-list' => '[', 'suffix-list' => ']', 'suffix-list-end' => true, ]; if (!is_array($source)) { $source = (array) $source; } if (array_is_list($source)) { $currentPrefix = $opt['prefix-list']; $currentSuffix = $opt['suffix-list']; $currentSuffixEnd = $opt['suffix-list-end']; } else { $currentPrefix = $opt['prefix']; $currentSuffix = $opt['suffix']; $currentSuffixEnd = $opt['suffix-end']; } $currentName = $start; foreach ($source as $key => $val) { $currentName .= $currentPrefix.$key; if (is_array($val) && !empty($val)) { $currentName .= "{$currentSuffix}"; self::flattenArray($val, $destination, $currentName); } else { if ($currentSuffixEnd) { $currentName .= $currentSuffix; } $destination[$currentName] = self::toString($val); } $currentName = $start; } } }