Skip to content

Commit

Permalink
Implemented "php" tags for structs (#20)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
DasJott authored and elliotchance committed Jan 22, 2020
1 parent dfe9e13 commit 7f8a11c
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 29 deletions.
41 changes: 21 additions & 20 deletions consume.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package phpserialize

import (
"errors"
"fmt"
"reflect"
"strconv"
)
Expand Down Expand Up @@ -170,19 +169,9 @@ func consumeObjectAsMap(data []byte, offset int) (
return result, offset + 1, nil
}

func setField(obj interface{}, name string, value interface{}) error {
structValue := reflect.ValueOf(obj).Elem()

// We need to uppercase the first letter for compatibility.
// The Marshal() function does the opposite of this.
structFieldValue := structValue.FieldByName(upperCaseFirstLetter(name))

func setField(structFieldValue reflect.Value, value interface{}) error {
if !structFieldValue.IsValid() {
return fmt.Errorf("no such field: %s in obj", name)
}

if !structFieldValue.CanSet() {
return fmt.Errorf("cannot set %s field value", name)
return nil
}

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

case reflect.Struct:
m := val.Interface().(map[interface{}]interface{})
fillStruct(structFieldValue.Addr().Interface(), m)
fillStruct(structFieldValue, m)

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

// https://stackoverflow.com/questions/26744873/converting-map-to-struct
func fillStruct(obj interface{}, m map[interface{}]interface{}) error {
for k, v := range m {
err := setField(obj, k.(string), v)
if err != nil {
return err
func fillStruct(obj reflect.Value, m map[interface{}]interface{}) error {
tt := obj.Type()
for i := 0; i < obj.NumField(); i++ {
field := obj.Field(i)
if !field.CanSet() {
continue
}
var key string
if tag := tt.Field(i).Tag.Get("php"); tag == "-" {
continue
} else if tag != "" {
key = tag
} else {
key = lowerCaseFirstLetter(tt.Field(i).Name)
}
if v, ok := m[key]; ok {
setField(field, v)
}
}

return nil
}

func consumeObject(data []byte, offset int, v interface{}) (int, error) {
func consumeObject(data []byte, offset int, v reflect.Value) (int, error) {
if !checkType(data, 'O', offset) {
return -1, errors.New("not an object")
}
Expand Down
2 changes: 1 addition & 1 deletion example/test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package main

import (
"github.com/elliotchance/phpserialize"
"fmt"
"github.com/elliotchance/phpserialize"
)

func main() {
Expand Down
11 changes: 8 additions & 3 deletions serialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,12 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) {
// with an uppercase letter) we must change it to lower case. If
// you really do want it to be upper case you will have to wait
// for when tags are supported on individual fields.
fieldName := lowerCaseFirstLetter(typeOfValue.Field(i).Name)
fieldName := typeOfValue.Field(i).Tag.Get("php")
if fieldName == "-" {
continue
} else if fieldName == "" {
fieldName = lowerCaseFirstLetter(typeOfValue.Field(i).Name)
}
buffer.Write(MarshalString(fieldName))

m, err := Marshal(f.Interface(), options)
Expand All @@ -188,11 +193,11 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) {
// Marshal is the canonical way to perform the equivalent of serialize() in PHP.
// It can handle encoding scalar types, slices and maps.
func Marshal(input interface{}, options *MarshalOptions) ([]byte, error) {

if options == nil {
options = DefaultMarshalOptions()
}

// []byte is a special case because all strings (binary and otherwise)
// are handled as strings in PHP.
if bytesToEncode, ok := input.([]byte); ok {
Expand Down
15 changes: 15 additions & 0 deletions serialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ type struct1 struct {
Baz string
}

type structTag struct {
Foo Struct2 `php:"bar"`
Bar int `php:"foo"`
hidden bool
Balu string `php:"baz"`
Ignored string `php:"-"`
}

type Struct2 struct {
Qux float64
}
Expand Down Expand Up @@ -125,6 +133,13 @@ var marshalTests = map[string]marshalTest{
nil,
},

// encode object (struct with tags)
"structTag{Bar int, Foo Struct2{Qux float64}, hidden bool, Balu string}": {
structTag{Struct2{1.23}, 10, true, "yay", ""},
[]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\";}"),
nil,
},

// stdClassOnly
"struct1{Foo int, Bar Struct2{Qux float64}, hidden bool}: OnlyStdClass = true": {
struct1{10, Struct2{1.23}, true, "yay"},
Expand Down
7 changes: 3 additions & 4 deletions unserialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func UnmarshalAssociativeArray(data []byte) (map[interface{}]interface{}, error)
return result, err
}

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

switch value.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v, err := UnmarshalInt(data)
if err != nil {
return err
Expand Down Expand Up @@ -195,7 +194,7 @@ func Unmarshal(data []byte, v interface{}) error {
return nil

case reflect.Struct:
err := UnmarshalObject(data, v)
err := UnmarshalObject(data, value)
if err != nil {
return err
}
Expand Down
23 changes: 23 additions & 0 deletions unserialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,29 @@ func TestUnmarshalObject(t *testing.T) {
}
}

func TestUnmarshalObjectWithTags(t *testing.T) {
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\";}"
var result structTag
err := phpserialize.Unmarshal([]byte(data), &result)
expectErrorToNotHaveOccurred(t, err)

if result.Bar != 10 {
t.Errorf("Expected %v, got %v", 10, result.Bar)
}

if result.Foo.Qux != 1.23 {
t.Errorf("Expected %v, got %v", 1.23, result.Foo.Qux)
}

if result.Balu != "yay" {
t.Errorf("Expected %v, got %v", "yay", result.Balu)
}

if result.Ignored != "" {
t.Errorf("Expected %v, got %v", "yay", result.Ignored)
}
}

func TestUnmarshalObjectIntoMap(t *testing.T) {
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\";}"
var result map[interface{}]interface{}
Expand Down
2 changes: 1 addition & 1 deletion util_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package phpserialize_test

import (
"testing"
"github.com/elliotchance/phpserialize"
"reflect"
"testing"
)

func TestStringifyKeysOnEmptyMap(t *testing.T) {
Expand Down

0 comments on commit 7f8a11c

Please sign in to comment.