Skip to content

Commit 7f8a11c

Browse files
DasJottelliotchance
authored andcommitted
Implemented "php" tags for structs (#20)
If you marshal or unmarshal from/into a struct, you may want to use go tags for it, instead of staticly using the struct field names just changing the first letter to upper/lower case. Example: type Target struct { FirstName string `php:"vorname"` LastName string `php:"nachname"` } This will correctly be filled with the according fields of serialized php object. Also unmarshaling will not fail if a struct field does not exist, because you may want to skip some fields from php objects.
1 parent dfe9e13 commit 7f8a11c

7 files changed

+72
-29
lines changed

consume.go

+21-20
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package phpserialize
22

33
import (
44
"errors"
5-
"fmt"
65
"reflect"
76
"strconv"
87
)
@@ -170,19 +169,9 @@ func consumeObjectAsMap(data []byte, offset int) (
170169
return result, offset + 1, nil
171170
}
172171

173-
func setField(obj interface{}, name string, value interface{}) error {
174-
structValue := reflect.ValueOf(obj).Elem()
175-
176-
// We need to uppercase the first letter for compatibility.
177-
// The Marshal() function does the opposite of this.
178-
structFieldValue := structValue.FieldByName(upperCaseFirstLetter(name))
179-
172+
func setField(structFieldValue reflect.Value, value interface{}) error {
180173
if !structFieldValue.IsValid() {
181-
return fmt.Errorf("no such field: %s in obj", name)
182-
}
183-
184-
if !structFieldValue.CanSet() {
185-
return fmt.Errorf("cannot set %s field value", name)
174+
return nil
186175
}
187176

188177
val := reflect.ValueOf(value)
@@ -198,7 +187,7 @@ func setField(obj interface{}, name string, value interface{}) error {
198187

199188
case reflect.Struct:
200189
m := val.Interface().(map[interface{}]interface{})
201-
fillStruct(structFieldValue.Addr().Interface(), m)
190+
fillStruct(structFieldValue, m)
202191

203192
default:
204193
structFieldValue.Set(val)
@@ -208,18 +197,30 @@ func setField(obj interface{}, name string, value interface{}) error {
208197
}
209198

210199
// https://stackoverflow.com/questions/26744873/converting-map-to-struct
211-
func fillStruct(obj interface{}, m map[interface{}]interface{}) error {
212-
for k, v := range m {
213-
err := setField(obj, k.(string), v)
214-
if err != nil {
215-
return err
200+
func fillStruct(obj reflect.Value, m map[interface{}]interface{}) error {
201+
tt := obj.Type()
202+
for i := 0; i < obj.NumField(); i++ {
203+
field := obj.Field(i)
204+
if !field.CanSet() {
205+
continue
206+
}
207+
var key string
208+
if tag := tt.Field(i).Tag.Get("php"); tag == "-" {
209+
continue
210+
} else if tag != "" {
211+
key = tag
212+
} else {
213+
key = lowerCaseFirstLetter(tt.Field(i).Name)
214+
}
215+
if v, ok := m[key]; ok {
216+
setField(field, v)
216217
}
217218
}
218219

219220
return nil
220221
}
221222

222-
func consumeObject(data []byte, offset int, v interface{}) (int, error) {
223+
func consumeObject(data []byte, offset int, v reflect.Value) (int, error) {
223224
if !checkType(data, 'O', offset) {
224225
return -1, errors.New("not an object")
225226
}

example/test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package main
22

33
import (
4-
"github.com/elliotchance/phpserialize"
54
"fmt"
5+
"github.com/elliotchance/phpserialize"
66
)
77

88
func main() {

serialize.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,12 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) {
165165
// with an uppercase letter) we must change it to lower case. If
166166
// you really do want it to be upper case you will have to wait
167167
// for when tags are supported on individual fields.
168-
fieldName := lowerCaseFirstLetter(typeOfValue.Field(i).Name)
168+
fieldName := typeOfValue.Field(i).Tag.Get("php")
169+
if fieldName == "-" {
170+
continue
171+
} else if fieldName == "" {
172+
fieldName = lowerCaseFirstLetter(typeOfValue.Field(i).Name)
173+
}
169174
buffer.Write(MarshalString(fieldName))
170175

171176
m, err := Marshal(f.Interface(), options)
@@ -188,11 +193,11 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) {
188193
// Marshal is the canonical way to perform the equivalent of serialize() in PHP.
189194
// It can handle encoding scalar types, slices and maps.
190195
func Marshal(input interface{}, options *MarshalOptions) ([]byte, error) {
191-
196+
192197
if options == nil {
193198
options = DefaultMarshalOptions()
194199
}
195-
200+
196201
// []byte is a special case because all strings (binary and otherwise)
197202
// are handled as strings in PHP.
198203
if bytesToEncode, ok := input.([]byte); ok {

serialize_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ type struct1 struct {
1313
Baz string
1414
}
1515

16+
type structTag struct {
17+
Foo Struct2 `php:"bar"`
18+
Bar int `php:"foo"`
19+
hidden bool
20+
Balu string `php:"baz"`
21+
Ignored string `php:"-"`
22+
}
23+
1624
type Struct2 struct {
1725
Qux float64
1826
}
@@ -125,6 +133,13 @@ var marshalTests = map[string]marshalTest{
125133
nil,
126134
},
127135

136+
// encode object (struct with tags)
137+
"structTag{Bar int, Foo Struct2{Qux float64}, hidden bool, Balu string}": {
138+
structTag{Struct2{1.23}, 10, true, "yay", ""},
139+
[]byte("O:9:\"structTag\":4:{s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:1.23;}s:3:\"foo\";i:10;s:3:\"baz\";s:3:\"yay\";}"),
140+
nil,
141+
},
142+
128143
// stdClassOnly
129144
"struct1{Foo int, Bar Struct2{Qux float64}, hidden bool}: OnlyStdClass = true": {
130145
struct1{10, Struct2{1.23}, true, "yay"},

unserialize.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func UnmarshalAssociativeArray(data []byte) (map[interface{}]interface{}, error)
113113
return result, err
114114
}
115115

116-
func UnmarshalObject(data []byte, v interface{}) error {
116+
func UnmarshalObject(data []byte, v reflect.Value) error {
117117
_, err := consumeObject(data, 0, v)
118118
return err
119119
}
@@ -122,8 +122,7 @@ func Unmarshal(data []byte, v interface{}) error {
122122
value := reflect.ValueOf(v).Elem()
123123

124124
switch value.Kind() {
125-
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
126-
reflect.Int64:
125+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
127126
v, err := UnmarshalInt(data)
128127
if err != nil {
129128
return err
@@ -195,7 +194,7 @@ func Unmarshal(data []byte, v interface{}) error {
195194
return nil
196195

197196
case reflect.Struct:
198-
err := UnmarshalObject(data, v)
197+
err := UnmarshalObject(data, value)
199198
if err != nil {
200199
return err
201200
}

unserialize_test.go

+23
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,29 @@ func TestUnmarshalObject(t *testing.T) {
499499
}
500500
}
501501

502+
func TestUnmarshalObjectWithTags(t *testing.T) {
503+
data := "O:7:\"struct1\":3:{s:3:\"foo\";i:10;s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:1.23;}s:3:\"baz\";s:3:\"yay\";}"
504+
var result structTag
505+
err := phpserialize.Unmarshal([]byte(data), &result)
506+
expectErrorToNotHaveOccurred(t, err)
507+
508+
if result.Bar != 10 {
509+
t.Errorf("Expected %v, got %v", 10, result.Bar)
510+
}
511+
512+
if result.Foo.Qux != 1.23 {
513+
t.Errorf("Expected %v, got %v", 1.23, result.Foo.Qux)
514+
}
515+
516+
if result.Balu != "yay" {
517+
t.Errorf("Expected %v, got %v", "yay", result.Balu)
518+
}
519+
520+
if result.Ignored != "" {
521+
t.Errorf("Expected %v, got %v", "yay", result.Ignored)
522+
}
523+
}
524+
502525
func TestUnmarshalObjectIntoMap(t *testing.T) {
503526
data := "O:7:\"struct1\":3:{s:3:\"foo\";i:10;s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:1.23;}s:3:\"baz\";s:3:\"yay\";}"
504527
var result map[interface{}]interface{}

util_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package phpserialize_test
22

33
import (
4-
"testing"
54
"github.com/elliotchance/phpserialize"
65
"reflect"
6+
"testing"
77
)
88

99
func TestStringifyKeysOnEmptyMap(t *testing.T) {

0 commit comments

Comments
 (0)