package gophplib import ( "encoding/base64" "fmt" "math" "reflect" ) // Base64Encode emulates the functionality of PHP 5.6's base64_encode function. // For more information, see the [official PHP documentation]. // In PHP 5.6, an error is triggered if the input size is excessively large due to memory limitations. // This Go implementation includes similar checks to emulate PHP's memory limitation conditions. // Additionally, this function converts different types of variables to a string, following PHP's dynamic typing approach. // After ensuring the memory constraints are met and converting the input to a string, // it uses the EncodeToString function from the encoding/base64 package to perform the Base64 encoding. // The result is a Base64 encoded string, consistent with PHP's output for the same input. // For more detailed information about the EncodeToString function in the package encoding/base64, // see the [encoding/base64's EncodeToString documentation] // // This function returns error if given argument is not one of following: // string, int, int64, float64, bool, nil, and any type which does not implement // interface { toString() string }. // // PHP references: // - base64_encode definition: // https://github.com/php/php-src/blob/php-5.6.40/ext/standard/base64.c#L224-L241 // - base64_encode implementation: // https://github.com/php/php-src/blob/4b8f72da5dfb201af4e82dee960261d8657e414f/ext/standard/base64.c#L56-L106 // // Test Cases : // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/url/base64_encode_basic_001.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/url/base64_encode_basic_002.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/url/base64_encode_error_001.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/url/base64_encode_variation_001.phpt // // [official PHP documentation]: https://www.php.net/manual/en/function.base64-encode // [encoding/base64's EncodeToString documentation]: https://pkg.go.dev/encoding/base64#Encoding.EncodeToString func Base64Encode(value any) (string, error) { // Convert a value to string characterString, err := zendParseArgAsString(value) if err != nil { return "", fmt.Errorf("unsupported type : %s", reflect.TypeOf(value)) } // Base64 encoding converts 3 bytes into 4 bytes ASCII characters, // and adds padding 2 bytes if the converted data is not a multiple of 3. // In PHP, the base64_encode function includes a memory allocation limit check // to prevent potential overflow issues due to the increase in data size during encoding. // In Go, such checks are generally not required thanks to its robust memory management system. // However, to maintain exact behavioral parity with the PHP implementation, // this function includes memory limit check too. if (len(characterString)+2)/3 > math.MaxInt32/4 { return "", fmt.Errorf("string too long, maximum is 1610612733") } encodedString := base64.StdEncoding.EncodeToString([]byte(characterString)) return encodedString, nil }
package gophplib import ( "fmt" "reflect" "strings" "github.com/elliotchance/orderedmap/v2" ) // Implode replicates the behavior of PHP 5.6's implode function in GO. // This function concatenates the elements of an array into a single string using a specified separator. // For more information, see the [official PHP documentation]. // // The function supports flexible argument patterns to accommodate various use cases. // - arg1: Can be either a string (used as the separator) or an array of elements to be joined. // - options: Optional. When provided, the first element is used as arg2. // If arg1 is a string, arg2 serves as the array of elements to be joined. // If arg1 is an array, arg2 serves as the separator. // // Behavior: // // 1. If only arg1 is provided: // - If arg1 is an array, the function joins the elements using an empty string as the default separator. // - If arg1 is not an array, the function returns an error. // // 2. If both arg1 and arg2 are provided: // - If arg1 is an array, arg2 is converted to string and used as the separator. // - If arg1 is not an array and arg2 is an array, arg1 is converted to a string // and used as the separator, with arg2 being the array to implode. // - If neither arg1 nor arg2 is an array, the function returns an error. // // Non-string elements within the array are converted to strings using a ConvertToString function // before joining. // Due to language differences between PHP and Go, the implode function support OrderedMap type from the [orderedmap library], // ensuring ordered map functionality. When imploding map types, please utilize the OrderedMap type from the [orderedmap library] // to maintain element order. If you use map type, not OrderedMap type, the order of the results cannot be guaranteed. // // reference: // - implode: https://github.com/php/php-src/blob/php-5.6.40/ext/standard/string.c#L1229-L1269 // - php_implode: https://github.com/php/php-src/blob/php-5.6.40/ext/standard/string.c#L1141-L1224 // // Test cases: // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/implode.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/implode1.phpt // // [official PHP documentation]: https://www.php.net/manual/en/function.implode.php // [orderedmap library]: https://pkg.go.dev/github.com/elliotchance/orderedmap/v2 func Implode(arg1 any, options ...any) (string, error) { var delim string var arr []any // Check arg1 is one of array, slice, map, or ordered ap isArg1CollectionType := isCollectionType(arg1) // Check if options is not provided if len(options) == 0 { if !isArg1CollectionType { return "", fmt.Errorf("argument must be one of array, slice, or ordered map, but got %v", reflect.TypeOf(arg1)) } arr = aggregateValues(arg1) } else { arg2 := options[0] // Check arg2 is one of array, slice, or ordered map isArg2CollectionType := isCollectionType(arg2) if isArg1CollectionType { delim, _ = ConvertToString(arg2) arr = aggregateValues(arg1) } else if !isArg1CollectionType && isArg2CollectionType { delim, _ = ConvertToString(arg1) arr = aggregateValues(arg2) } else { return "", fmt.Errorf("invalid arguments passed, got %v, %v", reflect.TypeOf(arg1), reflect.TypeOf(arg2)) } } // Join arr elements with a delim var builder strings.Builder if len(arr) == 0 { return "", nil } for i, item := range arr { str, err := ConvertToString(item) if err != nil { return "", fmt.Errorf("unsupported type in array : %v", reflect.TypeOf(item)) } else { builder.WriteString(str) } if i < len(arr)-1 { builder.WriteString(delim) } } return builder.String(), nil } // isOrderedMap checks if the argument is an instance of ordered map func isOrderedMap(arg any) bool { switch arg.(type) { case orderedmap.OrderedMap[any, any], *orderedmap.OrderedMap[any, any]: return true default: return false } } // isCollectionType checks if the argument is either an array, a slice, a map or ordered map func isCollectionType(arg any) bool { if arg == nil { return false } argType := reflect.TypeOf(arg).Kind() return isOrderedMap(arg) || argType == reflect.Slice || argType == reflect.Array || argType == reflect.Map } // aggregateValues extracts the stored value from different types of source: // ordered map, map, slice and array. It gathers there values into an arr and returns it. func aggregateValues(source any) []any { if isOrderedMap(source) { var om *orderedmap.OrderedMap[any, any] switch tmp := source.(type) { case orderedmap.OrderedMap[any, any]: // If source is an OrderedMap struct, use address of source om = &tmp case *orderedmap.OrderedMap[any, any]: om = tmp } arr := make([]any, 0, om.Len()) for el := om.Front(); el != nil; el = el.Next() { arr = append(arr, el.Value) } return arr } else { v := reflect.ValueOf(source) arr := make([]any, 0, v.Len()) switch v.Kind() { case reflect.Map: for _, value := range v.MapKeys() { arr = append(arr, v.MapIndex(value).Interface()) } case reflect.Slice, reflect.Array: for i := 0; i < v.Len(); i++ { arr = append(arr, v.Index(i).Interface()) } } return arr } }
package gophplib import ( "fmt" "reflect" ) // Ord is a ported functions that works exactly the same as PHP 5.6's ord function. // In PHP 5.6, when the ord() function is used with a data type other // than a string, it automatically converts the given variable into a string // before processing it. To achieve the same behavior in Go, // this function converts an argument to string using the zendParseArgAsString() function. // For more information, see the [official PHP documentation]. // // This function returns error if given argument is not one of following: // string, int, int64, float64, bool, nil, and any type which does not implement // interface { toString() string }. // // Reference : // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/string.c#L2666-L2676 // // Test Cases: // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/ord_basic.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/ord_error.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/ord_variation1.phpt // // [official PHP documentation]: https://www.php.net/manual/en/function.ord.php func Ord(character any) (byte, error) { // Convert a character to string characterString, err := zendParseArgAsString(character) if err != nil { return 0, fmt.Errorf("unsupported type : %s", reflect.TypeOf(character)) } // Check if the characterString is not empty if len(characterString) > 0 { // Return the first byte of input argument's string representation return []byte(characterString)[0], nil } // Return for empty strings return 0, nil }
package gophplib import ( "bytes" "strconv" "strings" "github.com/elliotchance/orderedmap/v2" ) // ParseStr is a ported function that works exactly the same as PHP's parse_str // function. For more information, see the [official PHP documentation]. // // All keys of the returned orderedmap.OrderedMap are either string or int. Type of all values of // the returned orderedmap.OrderedMap are either string or "orderedmap.OrderedMap[string | int]RetVal". // For more information about orderedmap libaray, see the [orderedmap documentation]. // // Reference: // - https://www.php.net/manual/en/function.parse-str.php // - https://github.com/php/php-src/blob/php-5.6.40/main/php_variables.c#L450-L496 // - https://github.com/php/php-src/blob/php-8.3.0/main/php_variables.c#L523-L568 // // [official PHP documentation]: https://www.php.net/manual/en/function.parse-str.php // [orderedmap documentation]: https://pkg.go.dev/github.com/elliotchance/orderedmap/v2@v2.2.0 func ParseStr(input string) orderedmap.OrderedMap[any, any] { ret := newPHPArray() // Split input with '&' pairs := strings.Split(input, "&") for _, pair := range pairs { // Skip empty pair if pair == "" { continue } // Cut pair with '=' key, value, _ := strings.Cut(pair, "=") registerVariableSafe(Urldecode(key), Urldecode(value), ret) } return ret.intoMap() } // registerVariableSafe is a ported function that works exactly the same as // PHP's php_register_variable_safe function. // // Reference: // - https://github.com/php/php-src/blob/php-5.6.40/main/php_variables.c#L59-L233 // - https://github.com/php/php-src/blob/php-8.3.0/main/php_variables.c#L90-L314 func registerVariableSafe(key, value string, track *phpSymtable) { // NOTE: key is "var_name", value is "val", track is "track_vars_array" in // below PHP version's function signature. // // PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *track_vars_array) // ignore leading spaces in the variable name key = strings.TrimLeft(key, " ") // Prepare variable name // NOTE: key_new is "var" and "var_orig" in the original PHP codes. key_new := []byte(key) // ensure that we don't have spaces or dots in the variable name (not binary safe) is_array := false index_slice := []byte(nil) // index_slice is "ip" in the original PHP codes. for i, c := range key_new { if c == ' ' || c == '.' { key_new[i] = '_' } else if c == '[' { is_array = true key_new, index_slice = key_new[:i], key_new[i:] break } } // empty variable name, or variable name with a space in it if len(key_new) == 0 { return } index := key_new if is_array { // We do not perform max nesting level check here idx := 0 // idx is offset of "ip" pointer in the original PHP codes. for { idx++ idx_s := idx // idx_next is "index_s" in the original PHP codes. if isAsciiWhitespace(index_slice[idx]) { idx++ } if index_slice[idx] == ']' { idx_s = -1 } else { ret := bytes.IndexByte(index_slice[idx:], ']') if ret == -1 { // not an index; un-terminate the var name index_slice[idx_s-1] = '_' // NOTE: concat of `index` and `index_slice[idx_s-1:]` only // occurs when idx_s == 1. if index != nil && idx_s == 1 { index = append(index, index_slice...) } goto plain_var } idx += ret } var subdict *phpSymtable if index == nil { subdict = newPHPArray() track.setNext(subdict) } else { value, ok := track.get(index) if !ok { subdict = newPHPArray() track.set(index, subdict) } else { // References for origianl PHP codes of here: // - https://www.phpinternalsbook.com/php7/zvals/memory_management.html // - https://www.phpinternalsbook.com/php7/zvals/basic_structure.html underlying, ok := value.(*phpSymtable) if !ok { subdict = newPHPArray() track.set(index, subdict) } else { subdict = underlying } } } track = subdict if idx_s != -1 { index = index_slice[idx_s:idx] } else { index = nil } idx++ if idx < len(index_slice) && index_slice[idx] == '[' { // Do nothing } else { goto plain_var } } } plain_var: if index == nil { track.setNext(value) } else { track.set(index, value) } } // phpSymtable is a orderedmap.OrderedMap[any, any] which behaves like PHP's array. It maintains // internal next (i.e. nNextFreeElement of PHP) state and it automatically // converts numeric string keys to integer keys. type phpSymtable struct { next int // Key is either string or int. // Value is either string or *phpSymtable. d orderedmap.OrderedMap[any, any] } func newPHPArray() *phpSymtable { return &phpSymtable{ next: 0, d: *orderedmap.NewOrderedMap[any, any](), } } // It returns an orderedmap.OrderedMap[any, any] whose keys are either string or int, and whose // values (RetVal) are either string or "orderedmap.OrderedMap[string | int, RetVal]". func (p *phpSymtable) intoMap() orderedmap.OrderedMap[any, any] { ret := *orderedmap.NewOrderedMap[any, any]() for el := p.d.Front(); el != nil; el = el.Next() { key := el.Key value := el.Value if sub, ok := el.Value.(*phpSymtable); ok { ret.Set(key, sub.intoMap()) } else { ret.Set(key, value) } } return ret } func (p *phpSymtable) get(key []byte) (any, bool) { k := phpNumericOrString(key) v, ok := p.d.Get(k) return v, ok } func (p *phpSymtable) set(key []byte, value any) { k := phpNumericOrString(key) if numeric, ok := k.(int); ok { p.next = maxInt(p.next, numeric+1) } p.d.Set(k, value) } func (p *phpSymtable) setNext(value any) { p.d.Set(p.next, value) p.next++ } func maxInt(a, b int) int { if a > b { return a } return b } func phpNumericOrString(input []byte) any { str := string(input) if !zendHandleNumericStr(str) { return str } num, err := strconv.Atoi(str) // Check if it overflows if err != nil { return str } return num } // zendHandleNumericStr is a ported function that works exactly the same as // PHP's _zend_handle_numeric_str function. // // It returns true if the input string meets all the following conditions: // - It is a signed integer string without leading zeros. (positive sign is // not allowed, only negative sign is allowed) // - It is not a negative zero. // // It behaves same with regexp.MustCompile(`^-?[1-9][0-9]*$|^0$`).MatchString // // References: // - https://github.com/php/php-src/blob/php-8.3.0/Zend/zend_hash.h#L388-L404 // - https://github.com/php/php-src/blob/php-8.3.0/Zend/zend_hash.c#L3262-L3299 func zendHandleNumericStr(s string) bool { // Handle few cases first to make further checks simpler switch s { case "0": return true case "", "-": return false } // Check for negative sign begin := 0 if s[0] == '-' { begin = 1 } // Ensure the first character isn't '0' if s[begin] == '0' { return false } // Check that all characters are digits for _, ch := range s[begin:] { if ch < '0' || ch > '9' { return false } } return true } // isAsciiWhitespace is an ASCII-only version of C's isspace. // // References: // - https://en.cppreference.com/w/c/string/byte/isspace func isAsciiWhitespace(c byte) bool { return c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\v' }
package gophplib import ( "fmt" "reflect" ) // Strlen is a ported function that works exactly the same as PHP 5.6's strlen function. // In PHP 5.6, when the strlen() function is used with a data type other // than a string, it automatically converts the given variable into a string // before processing it. To achieve the same behavior in Go, // this function converts an argument to string using the zendParseArgAsString() function. // For more information, see the [official PHP documentation]. // // This function returns error if given argument is not one of following: // string, int, int64, float64, bool, nil, and any type which does not implement // interface { toString() string }. // // Reference : // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_builtin_functions.c#L479-L492 // // Test Case : // - https://github.dev/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/strlen.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/strlen_variation1.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/strlen_error.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/strlen_basic.phpt // // [official PHP documentation]: https://www.php.net/manual/en/function.strlen.php func Strlen(value any) (int, error) { // Convert a value to string characterString, err := zendParseArgAsString(value) if err != nil { return 0, fmt.Errorf("unsupported type : %s", reflect.TypeOf(value)) } return len(characterString), nil }
package gophplib import ( "fmt" "reflect" "strings" ) // Trim is a ported function that works exactly the same as PHP 5.6's trim // function. For more information, see the [official PHP documentation]. // // In PHP 5.6, when attempting to use the trim() function with a data type other // than a string, it automatically converts the requested variable into a string // before performing the trim. To achieve the same behavior in Go, this function // converts the requested data types into strings and then utilize the trim // function from the package strings. For more detailed information about the // trim function in the package strings, see the [strings's trim documentation] // // This function returns error if given argument is not one of following: // string, int, int64, float64, bool, nil, and any type which does not implement // interface { toString() string }. // // NOTE: This function does not support the second parameter of original parse_str yet. // It only strips the default characters (" \n\r\t\v\x00") // // References: // - https://www.php.net/manual/en/function.trim.php // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/string.c#L840-L850 // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L425-L470 // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L593-L661 // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L261-L302 // // Test Cases: // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/trim1.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/trim.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/trim_basic.phpt // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/trim_variation1.phpt // // [official PHP documentation]: https://www.php.net/manual/en/function.trim.php // [strings's trim documentation]: https://pkg.go.dev/strings#Trim func Trim(value any) (ret string, err error) { // Convert a value to string characterString, err := zendParseArgAsString(value) if err != nil { err = fmt.Errorf("unsupported type : %s", reflect.TypeOf(value)) return } const charSet = " \n\r\t\v\x00" ret = strings.Trim(characterString, charSet) return }
package gophplib import ( "strings" ) // Urldecode is a ported function that works exactly the same as PHP's urldecode // function. For more information, see the [official PHP documentation]. // // Unlike net/url's QueryUnescape function, this function *never* fails. Instead // of returning an error, it leaves invalid percent encoded sequences as is. // And contrary to its name, it does not follow percent encoding specification // of RFC 3986 since it decodes '+' to ' '. This is done to be compatible with // PHP's urldecode function. // // References: // - https://www.php.net/manual/en/function.urldecode.php // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/url.c#L513-L561 // - https://github.com/php/php-src/blob/php-8.3.0/ext/standard/url.c#L578-L618 // // [official PHP documentation]: https://www.php.net/manual/en/function.urldecode.php func Urldecode(input string) string { buf := []byte(input) length := len(buf) j := 0 for i := 0; i < length; i, j = i+1, j+1 { if buf[i] == '+' { buf[j] = ' ' } else if buf[i] == '%' && i+2 < length && isxdigit(buf[i+1]) && isxdigit(buf[i+2]) { buf[j] = htoi(buf[i+1], buf[i+2]) i += 2 } else { buf[j] = buf[i] } } return strings.ToValidUTF8(string(buf[:j]), "�") } // isxdigit is a ported function that works exactly the same as C's isxdigit // function. // // References: // - https://en.cppreference.com/w/c/string/byte/isxdigit func isxdigit(c byte) bool { return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' } // htoi is a ported function that works exactly the same as PHP's php_htoi // function. It returns the byte value of the hexadecimal number represented by // the two bytes hi and lo. // // It expects both hi and lo to be valid hexadecimal digits. (ex: '0'-'9', // 'a'-'f', 'A'-'F') Otherwise, the result is undefined. // // References: // - https://github.com/php/php-src/blob/php-8.3.0/ext/standard/url.c#L426-L444 // - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/url.c#L407-L426 func htoi(hi, lo byte) byte { if 'A' <= hi && hi <= 'Z' { hi += 'a' - 'A' } if '0' <= hi && hi <= '9' { hi -= '0' } else { hi -= 'a' - 10 } if 'A' <= lo && lo <= 'Z' { lo += 'a' - 'A' } if '0' <= lo && lo <= '9' { lo -= '0' } else { lo -= 'a' - 10 } return hi*16 + lo }
package gophplib import ( "fmt" "reflect" ) // zendParseArgAsString attempts to replicate the behavior of the 'zend_parse_arg_impl' function // from PHP 5.6, specifically for the case where the 'spec' parameter is "s". // It handles conversion of different types to string in a way that aligns with PHP's type juggling rules, // calling ConvertToString to manage string, int, float, and bool types, akin to PHP's _convert_to_string. // // This function returns error if given argument is not one of following: // string, int, int8, int16, int32, int64, float32, float64, bool, nil // and any type which does not implement interface { toString() string }. // // Reference : // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L685-L713 // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L425-L470 // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L593-L661 // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L261-L301 func zendParseArgAsString(value any) (string, error) { var str string switch v := value.(type) { case string, int, int8, int16, int32, int64, float32, float64, bool: return ConvertToString(value) case nil: // TODO: handle check_null str = "" case toStringAble: // For types implementing toString(), get the value of toString() str = v.toString() default: return "", fmt.Errorf("unsupported type : %s", reflect.TypeOf(v)) } return str, nil }
package gophplib import ( "database/sql" "fmt" "math" "net" "os" "reflect" ) type toStringAble interface { toString() string } // floatToString converts a float64 to a string based on the PHP 5.6 rules. // - Allows up to a maximum of 14 digits, including both integer and decimal places. // - Remove trailing zeros from the fractional part // ex) 123.4000 → "123.4" // - Keep the values as is if the last digit is not 0. // ex) 123.45 → "123.45" // - If the integer part exceeds 14 digits, use exponential notation. // ex) 123456789123456.40 → "1.2345678901234e+14" // - If the total number of digits exceeds 14, truncate the decimal places. // ex) 123.45678901234 → "123.4567890123" // // Reference : // - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L627-L633 func floatToString(f64 float64) string { if math.IsNaN(f64) { return "NAN" } if math.IsInf(f64, 1) { return "INF" } if math.IsInf(f64, -1) { return "-INF" } return fmt.Sprintf("%.*G", 14, f64) } // ConvertToString attempts to convert the given value to string, emulating PHP 5.6'S _convert_to_string behavior. // Unlike PHP, which has built-in support for managing resource IDs for types like files and database connections, // Go does not inherently manage resource IDs. Due to this language difference, this function uses the values' pointer // address as the pseudo resource ID for identifiable resource types. // // This function returns error if given argument is not one of following: // string, int, int8, int16, int32, int64, float32, float64, bool, nil, *os.File, *net.Conn, and *sql.DB, // array, slice, map and any type which does not implement interface { toString() string }. // // NOTE : If the given argument's type is float32, it will be converted to float64 internally. // However, converting float32 to float64 may lead to precision loss. // Therefore, using float64 is recommended for higher accuracy. // // Reference: // - _convert_to_string implementation: // https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L593-L661 // - convert_object_to_type implementation: // https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L333-L357 func ConvertToString(value any) (string, error) { if value == nil { return "", nil } // handle basic and composite types dynamically switch v := value.(type) { case string: return v, nil case bool: if v { return "1", nil } else { return "", nil } case int, int8, int16, int32, int64: return fmt.Sprintf("%d", v), nil case float32: return floatToString(float64(v)), nil case float64: return floatToString(v), nil // check for special types such as a pointer of file, network, database resources case *os.File, *net.Conn, *sql.DB: // using a resource's address as the resource ID return fmt.Sprintf("Resource id %p", v), nil } // use reflection to handle array, slice, map types t := reflect.ValueOf(value).Kind() if t == reflect.Array || t == reflect.Slice || t == reflect.Map { return "Array", nil } if t == reflect.Struct { if result, ok := value.(toStringAble); ok { return result.toString(), nil } } // return an error for unsupported types. return "", fmt.Errorf("unsupported type : %T", value) }