Skip to content

Commit 47dbb64

Browse files
committed
Move serialization code to it's own class and introduce options.
1 parent 57ebe39 commit 47dbb64

File tree

6 files changed

+196
-150
lines changed

6 files changed

+196
-150
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Other changes:
77
- Added support for object de/serialization (`O:4:"name":n:{...}`).
88
- Added `[PhpClass()]` attribute.
99
- Added `StdClass` and `EnableTypeLookup` to deserialization options
10+
- Added options for `PhpSerialization.Serialize()`.
11+
- `ThrowOnCircularReferences` - whether or not to throw on ciruclar references, defaults to false (this might change in the future.)
1012
- Updated and adjusted some of the XML documentation.
1113

1214
# 0.4.0

PhpSerializerNET/Extensions/ArrayExtensions.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ This Source Code Form is subject to the terms of the Mozilla Public
44
file, You can obtain one at http://mozilla.org/MPL/2.0/.
55
**/
66

7-
8-
using System;
9-
using System.Collections;
10-
using System.Collections.Generic;
11-
using System.Globalization;
127
using System.Linq;
138
using System.Reflection;
149
using System.Text;

PhpSerializerNET/Extensions/TypeExtensions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ This Source Code Form is subject to the terms of the Mozilla Public
44
file, You can obtain one at http://mozilla.org/MPL/2.0/.
55
**/
66
using System;
7+
using System.Reflection;
78

89
namespace PhpSerializerNET {
9-
public static class TypeExtensions {
10+
internal static class TypeExtensions {
1011
/// <summary>
1112
/// Check if a given <see cref="System.Type"> implements IConvertible
1213
/// </summary>
@@ -15,5 +16,15 @@ public static class TypeExtensions {
1516
public static bool IsIConvertible(this Type type) {
1617
return typeof(IConvertible).IsAssignableFrom(type);
1718
}
19+
20+
internal static object GetValue(this MemberInfo member, object input) {
21+
if (member is PropertyInfo property) {
22+
return property.GetValue(input);
23+
}
24+
if (member is FieldInfo field) {
25+
return field.GetValue(input);
26+
}
27+
return null;
28+
}
1829
}
1930
}

PhpSerializerNET/PhpSerialization.cs

Lines changed: 1 addition & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,7 @@ This Source Code Form is subject to the terms of the Mozilla Public
44
file, You can obtain one at http://mozilla.org/MPL/2.0/.
55
**/
66

7-
using System;
8-
using System.Collections;
97
using System.Collections.Generic;
10-
using System.Globalization;
11-
using System.Linq;
12-
using System.Reflection;
13-
using System.Text;
148

159
namespace PhpSerializerNET {
1610
public static class PhpSerialization {
@@ -76,144 +70,7 @@ public static T Deserialize<T>(
7670
/// Objects may also be serialized into arrays, if their respective struct or class does not have the <see cref="PhpClass"/> attribute.
7771
/// </returns>
7872
public static string Serialize(object input) {
79-
var seenObjects = new List<object>();
80-
return Serialize(input, seenObjects);
81-
}
82-
83-
internal static object GetValue(this MemberInfo member, object input) {
84-
if (member is PropertyInfo property) {
85-
return property.GetValue(input);
86-
}
87-
if (member is FieldInfo field) {
88-
return field.GetValue(input);
89-
}
90-
return null;
91-
}
92-
93-
private static string SerializeMember(MemberInfo member, object input, List<object> seenObjects) {
94-
var propertyName = member.GetCustomAttribute<PhpPropertyAttribute>() != null
95-
? member.GetCustomAttribute<PhpPropertyAttribute>().Name
96-
: member.Name;
97-
return $"{Serialize(propertyName)}{Serialize(member.GetValue(input), seenObjects)}";
98-
}
99-
100-
private static string SerializeToObject(object input, List<object> seenObjects) {
101-
var className = input.GetType().GetCustomAttribute<PhpClass>().Name;
102-
if (string.IsNullOrEmpty(className)) {
103-
className = "stdClass";
104-
}
105-
StringBuilder output = new StringBuilder();
106-
var properties = input.GetType().GetProperties().Where(y => y.CanRead && y.GetCustomAttribute<PhpIgnoreAttribute>() == null);
107-
108-
output.Append("O:");
109-
output.Append(className.Length);
110-
output.Append(":\"");
111-
output.Append(className);
112-
output.Append("\":");
113-
output.Append(properties.Count());
114-
output.Append(":{");
115-
foreach (PropertyInfo property in properties) {
116-
output.Append(SerializeMember(property, input, seenObjects));
117-
}
118-
output.Append("}");
119-
return output.ToString();
120-
}
121-
122-
private static string Serialize(object input, List<object> seenObjects) {
123-
StringBuilder output = new StringBuilder();
124-
switch (input) {
125-
case long longValue: {
126-
return $"i:{longValue.ToString()};";
127-
}
128-
case int integerValue: {
129-
return $"i:{integerValue.ToString()};";
130-
}
131-
case double floatValue: {
132-
if (double.IsPositiveInfinity(floatValue)) {
133-
return $"d:INF;";
134-
}
135-
if (double.IsNegativeInfinity(floatValue)) {
136-
return $"d:-INF;";
137-
}
138-
if (double.IsNaN(floatValue)) {
139-
return $"d:NAN;";
140-
}
141-
return $"d:{floatValue.ToString(CultureInfo.InvariantCulture)};";
142-
}
143-
case string stringValue: {
144-
// Use the UTF8 byte count, because that's what the PHP implementation does:
145-
return $"s:{ASCIIEncoding.UTF8.GetByteCount(stringValue)}:\"{stringValue}\";";
146-
}
147-
case bool boolValue: {
148-
return boolValue ? "b:1;" : "b:0;";
149-
}
150-
case null: {
151-
return "N;";
152-
}
153-
case IDictionary dictionary: {
154-
if (seenObjects.Contains(input)) {
155-
// Terminate circular reference:
156-
// It might be better to make this optional. The PHP implementation seems to
157-
// throw an exception, from what I can tell
158-
return "N;";
159-
}
160-
if (dictionary.GetType().GenericTypeArguments.Count() > 0) {
161-
var keyType = dictionary.GetType().GenericTypeArguments[0];
162-
if (!keyType.IsIConvertible() && keyType != typeof(object)) {
163-
throw new Exception($"Can not serialize associative array with key type {keyType.FullName}");
164-
}
165-
}
166-
seenObjects.Add(input);
167-
output.Append($"a:{dictionary.Count}:");
168-
output.Append("{");
169-
170-
171-
foreach (DictionaryEntry entry in dictionary) {
172-
173-
output.Append($"{Serialize(entry.Key)}{Serialize(entry.Value, seenObjects)}");
174-
}
175-
output.Append("}");
176-
return output.ToString();
177-
}
178-
case IList collection: {
179-
if (seenObjects.Contains(input)) {
180-
return "N;"; // See above.
181-
}
182-
seenObjects.Add(input);
183-
output.Append($"a:{collection.Count}:");
184-
output.Append("{");
185-
for (int i = 0; i < collection.Count; i++) {
186-
output.Append(Serialize(i, seenObjects));
187-
output.Append(Serialize(collection[i], seenObjects));
188-
}
189-
output.Append("}");
190-
return output.ToString();
191-
}
192-
default: {
193-
if (seenObjects.Contains(input)) {
194-
return "N;"; // See above.
195-
}
196-
seenObjects.Add(input);
197-
var inputType = input.GetType();
198-
199-
if (inputType.GetCustomAttribute<PhpClass>() != null) // TODO: add option.
200-
{
201-
return SerializeToObject(input, seenObjects);
202-
}
203-
204-
IEnumerable<MemberInfo> members = inputType.IsValueType
205-
? inputType.GetFields().Where(y => y.IsPublic && y.GetCustomAttribute<PhpIgnoreAttribute>() == null)
206-
: inputType.GetProperties().Where(y => y.CanRead && y.GetCustomAttribute<PhpIgnoreAttribute>() == null);
207-
208-
output.Append($"a:{members.Count()}:");
209-
output.Append("{");
210-
foreach (var member in members) {
211-
output.Append(SerializeMember(member, input, seenObjects));
212-
}
213-
output.Append("}");
214-
return output.ToString();
215-
}
216-
}
73+
return new PhpSerializer().Serialize(input);
21774
}
21875
}
21976
}

PhpSerializerNET/PhpSerializer.cs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
This Source Code Form is subject to the terms of the Mozilla Public
3+
License, v. 2.0. If a copy of the MPL was not distributed with this
4+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
**/
6+
7+
using System;
8+
using System.Collections;
9+
using System.Collections.Generic;
10+
using System.Globalization;
11+
using System.Linq;
12+
using System.Reflection;
13+
using System.Text;
14+
15+
namespace PhpSerializerNET {
16+
internal class PhpSerializer {
17+
private PhpSerializiationOptions _options;
18+
private List<object> _seenObjects;
19+
20+
public PhpSerializer(PhpSerializiationOptions options = null) {
21+
_options = options ?? PhpSerializiationOptions.DefaultOptions;
22+
23+
_seenObjects = new();
24+
}
25+
26+
public string Serialize(object input) {
27+
StringBuilder output = new StringBuilder();
28+
switch (input) {
29+
case long longValue: {
30+
return $"i:{longValue.ToString()};";
31+
}
32+
case int integerValue: {
33+
return $"i:{integerValue.ToString()};";
34+
}
35+
case double floatValue: {
36+
if (double.IsPositiveInfinity(floatValue)) {
37+
return $"d:INF;";
38+
}
39+
if (double.IsNegativeInfinity(floatValue)) {
40+
return $"d:-INF;";
41+
}
42+
if (double.IsNaN(floatValue)) {
43+
return $"d:NAN;";
44+
}
45+
return $"d:{floatValue.ToString(CultureInfo.InvariantCulture)};";
46+
}
47+
case string stringValue: {
48+
// Use the UTF8 byte count, because that's what the PHP implementation does:
49+
return $"s:{ASCIIEncoding.UTF8.GetByteCount(stringValue)}:\"{stringValue}\";";
50+
}
51+
case bool boolValue: {
52+
return boolValue ? "b:1;" : "b:0;";
53+
}
54+
case null: {
55+
return "N;";
56+
}
57+
// TODO: There's enough shared code here to warrant refactoring.
58+
// Probably doing this in a default and then have another method for serializing the 3 options.
59+
case IDictionary dictionary: {
60+
if (_seenObjects.Contains(input)) {
61+
if (_options.ThrowOnCircularReferences){
62+
throw new ArgumentException("Input object has a circular reference.");
63+
}
64+
return "N;";
65+
}
66+
if (dictionary.GetType().GenericTypeArguments.Count() > 0) {
67+
var keyType = dictionary.GetType().GenericTypeArguments[0];
68+
if (!keyType.IsIConvertible() && keyType != typeof(object)) {
69+
throw new Exception($"Can not serialize into associative array with key type {keyType.FullName}");
70+
}
71+
}
72+
_seenObjects.Add(input);
73+
output.Append($"a:{dictionary.Count}:");
74+
output.Append("{");
75+
76+
77+
foreach (DictionaryEntry entry in dictionary) {
78+
79+
output.Append($"{this.Serialize(entry.Key)}{Serialize(entry.Value)}");
80+
}
81+
output.Append("}");
82+
return output.ToString();
83+
}
84+
case IList collection: {
85+
if (_seenObjects.Contains(input)) {
86+
if (_options.ThrowOnCircularReferences){
87+
throw new ArgumentException("Input object has a circular reference.");
88+
}
89+
return "N;";
90+
}
91+
_seenObjects.Add(input);
92+
output.Append($"a:{collection.Count}:");
93+
output.Append("{");
94+
for (int i = 0; i < collection.Count; i++) {
95+
output.Append(Serialize(i));
96+
output.Append(Serialize(collection[i]));
97+
}
98+
output.Append("}");
99+
return output.ToString();
100+
}
101+
default: {
102+
if (_seenObjects.Contains(input)) {
103+
if (_options.ThrowOnCircularReferences){
104+
throw new ArgumentException("Input object has a circular reference.");
105+
}
106+
return "N;"; // See above.
107+
}
108+
_seenObjects.Add(input);
109+
var inputType = input.GetType();
110+
111+
if (inputType.GetCustomAttribute<PhpClass>() != null) // TODO: add option.
112+
{
113+
return this.SerializeToObject(input);
114+
}
115+
116+
IEnumerable<MemberInfo> members = inputType.IsValueType
117+
? inputType.GetFields().Where(y => y.IsPublic && y.GetCustomAttribute<PhpIgnoreAttribute>() == null)
118+
: inputType.GetProperties().Where(y => y.CanRead && y.GetCustomAttribute<PhpIgnoreAttribute>() == null);
119+
120+
output.Append($"a:{members.Count()}:");
121+
output.Append("{");
122+
foreach (var member in members) {
123+
output.Append(this.SerializeMember(member, input));
124+
}
125+
output.Append("}");
126+
return output.ToString();
127+
}
128+
}
129+
}
130+
131+
private string SerializeToObject(object input) {
132+
var className = input.GetType().GetCustomAttribute<PhpClass>().Name;
133+
if (string.IsNullOrEmpty(className)) {
134+
className = "stdClass";
135+
}
136+
StringBuilder output = new StringBuilder();
137+
var properties = input.GetType().GetProperties().Where(y => y.CanRead && y.GetCustomAttribute<PhpIgnoreAttribute>() == null);
138+
139+
output.Append("O:");
140+
output.Append(className.Length);
141+
output.Append(":\"");
142+
output.Append(className);
143+
output.Append("\":");
144+
output.Append(properties.Count());
145+
output.Append(":{");
146+
foreach (PropertyInfo property in properties) {
147+
output.Append(this.SerializeMember(property, input));
148+
}
149+
output.Append("}");
150+
return output.ToString();
151+
}
152+
153+
private string SerializeMember(MemberInfo member, object input) {
154+
var propertyName = member.GetCustomAttribute<PhpPropertyAttribute>() != null
155+
? member.GetCustomAttribute<PhpPropertyAttribute>().Name
156+
: member.Name;
157+
return $"{this.Serialize(propertyName)}{this.Serialize(member.GetValue(input))}";
158+
}
159+
}
160+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
This Source Code Form is subject to the terms of the Mozilla Public
3+
License, v. 2.0. If a copy of the MPL was not distributed with this
4+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
**/
6+
7+
namespace PhpSerializerNET {
8+
9+
/// <summary>
10+
/// Options for serializing objects.
11+
/// </summary>
12+
public class PhpSerializiationOptions {
13+
/// <summary>
14+
/// Whether or not to throw on encountering a circular reference or to terminate it with null.
15+
/// Default false.
16+
/// </summary>
17+
public bool ThrowOnCircularReferences { get; set; } = false;
18+
19+
internal static PhpSerializiationOptions DefaultOptions = new();
20+
}
21+
}

0 commit comments

Comments
 (0)