Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.60% covered (warning)
72.60%
53 / 73
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Borsh
72.60% covered (warning)
72.60%
53 / 73
50.00% covered (danger)
50.00%
3 / 6
70.28
0.00% covered (danger)
0.00%
0 / 1
 serialize
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 serializeObject
41.67% covered (danger)
41.67%
5 / 12
0.00% covered (danger)
0.00%
0 / 1
9.96
 serializeField
72.00% covered (warning)
72.00%
18 / 25
0.00% covered (danger)
0.00%
0 / 1
18.30
 deserialize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 deserializeObject
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
8.83
 deserializeField
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
12
1<?php
2
3namespace Attestto\SolanaPhpSdk\Borsh;
4
5use Attestto\SolanaPhpSdk\Exceptions\TodoException;
6use Attestto\SolanaPhpSdk\Util\Buffer;
7
8class Borsh
9{
10    /**
11     * @param array $schema
12     * @param $object
13     * @return array
14     */
15    public static function serialize(
16        array $schema,
17        $object
18    ) : array
19    {
20        $writer = new BinaryWriter();
21        static::serializeObject($schema, $object, $writer);
22        return $writer->toArray();
23    }
24
25    /**
26     * @param array $schema
27     * @param $object
28     * @param BinaryWriter $writer
29     */
30    protected static function serializeObject(
31        array $schema,
32        $object,
33        BinaryWriter $writer
34    ) {
35        $objectSchema = $schema[get_class($object)] ?? null;
36        if (! $objectSchema) {
37            $class = get_class($object);
38            throw new BorshException("Class {$class} is missing in schema");
39        }
40
41        if ($objectSchema['kind'] === 'struct') {
42            foreach ($objectSchema['fields'] as list($fieldName, $fieldType)) {
43                static::serializeField($schema, $fieldName, $object->{$fieldName}, $fieldType, $writer);
44            }
45        } elseif ($objectSchema['kind'] === 'enum') {
46            throw new TodoException("TODO: Enums don't exist in PHP yet???");
47        } else {
48            $kind = $objectSchema['kind'];
49            $class = get_class($object);
50            throw new BorshException("Unexpected schema kind: {$kind} for {$class}");
51        }
52    }
53
54    /**
55     * @param array $schema
56     * @param $fieldName
57     * @param $value
58     * @param $fieldType
59     * @param BinaryWriter $writer
60     */
61    protected static function serializeField(
62        array $schema,
63        $fieldName,
64        $value,
65        $fieldType,
66        BinaryWriter $writer
67    ) {
68        if (is_string($fieldType)) {
69            $writer->{'write' . ucfirst($fieldType)}($value);
70        } elseif (is_array($fieldType) && isset($fieldType[0])) { // sequential array
71            if (is_int($fieldType[0])) {
72                if (sizeof($value) !== $fieldType[0]) {
73                    $sizeOf = sizeof($value);
74                    throw new BorshException("Expecting byte array of length {{$fieldType[0]}}, but got {{$sizeOf}} bytes");
75
76                }
77                $writer->writeFixedArray($value);
78            } elseif (sizeof($fieldType) === 2 && is_int($fieldType[1])) {
79                if (sizeof($value) !== $fieldType[1]) {
80                    $sizeOf = sizeof($value);
81                    throw new BorshException("Expecting byte array of length {{$fieldType[1]}}, but got {{$sizeOf}} bytes");
82
83                }
84
85                for ($i = 0; $i < $fieldType[1]; $i++) {
86                    static::serializeField($schema, null, $value[$i], $fieldType[0], $writer);
87                }
88            } else {
89                $writer->writeArray($value, fn ($item) => static::serializeField($schema, $fieldName, $item, $fieldType[0], $writer));
90            }
91        } elseif (isset($fieldType['kind'])) { // associative array
92            switch ($fieldType['kind']) {
93                case 'option':
94                    if ($value) {
95                        $writer->writeU8(1);
96                        static::serializeField($schema, $fieldName, $value, $fieldType['type'], $writer);
97                    } else {
98                        $writer->writeU8(0);
99                    }
100                    break;
101                default:
102                    throw new BorshException("FieldType {{$fieldType['kind']}} unrecognized");
103            }
104        } else {
105            static::serializeObject($schema, $value, $writer);
106        }
107    }
108
109    /**
110     * @param array $schema
111     * @param string $class
112     * @param array $buffer
113     */
114    public static function deserialize(
115        array $schema,
116        string $class,
117        array $buffer
118    )
119    {
120        $reader = new BinaryReader(Buffer::from($buffer));
121        return static::deserializeObject($schema, $class, $reader);
122    }
123
124    /**
125     * @param array $schema
126     * @param string $class
127     * @param BinaryReader $reader
128     */
129    protected static function deserializeObject(
130        array $schema,
131        string $class,
132        BinaryReader $reader
133    ) {
134        $objectSchema = $schema[$class] ?? null;
135        if (! $objectSchema) {
136            throw new BorshException("Class {$class} is missing in schema");
137        }
138
139        if ($objectSchema['kind'] === 'struct') {
140            if (! method_exists($class, 'borshConstructor')) {
141                throw new BorshException("Class {{$class}} does not implement borshConstructor. Please use the BorshDeserialize trait.");
142            }
143
144            $result = $class::borshConstructor();
145            foreach ($objectSchema['fields'] as list($fieldName, $fieldType)) {
146                $result->{$fieldName} = static::deserializeField($schema, $fieldName, $fieldType, $reader);
147                //$result->fields[$fieldName] = static::deserializeField($schema, $fieldName, $fieldType, $reader);
148            }
149            return $result;
150        }
151
152        if ($objectSchema['kind'] === 'enum') {
153            throw new TodoException("TODO: Enums don't exist in PHP yet???");
154        }
155
156        $kind = $objectSchema['kind'];
157        throw new BorshException("Unexpected schema kind: {$kind} for {$class}");
158    }
159
160    /**
161     * @param array $schema
162     * @param $fieldName
163     * @param $fieldType
164     * @param BinaryReader $reader
165     */
166    protected static function deserializeField(
167        array $schema,
168        $fieldName,
169        $fieldType,
170        BinaryReader $reader
171    ) {
172        if (is_string($fieldType) && ! class_exists($fieldType)) {
173            return $reader->{'read' . ucfirst($fieldType)}();
174        }
175
176        if (is_array($fieldType) && isset($fieldType[0])) { // sequential array
177            if (is_int($fieldType[0])) {
178                return $reader->readFixedArray($fieldType[0]);
179            } elseif (sizeof($fieldType) === 2 && is_int($fieldType[1])) {
180                $array = [];
181                for ($i = 0; $i < $fieldType[1]; $i++) {
182                    array_push($array, static::deserializeField($schema, null, $fieldType[0], $reader));
183                }
184                return $array;
185            } else {
186                return $reader->readArray(fn () => static::deserializeField($schema, $fieldName, $fieldType[0], $reader));
187            }
188        }
189
190        if (isset($fieldType['kind']) && $fieldType['kind'] === 'option') { // associative array
191            $option = $reader->readU8();
192            if ($option) {
193                return static::deserializeField($schema, $fieldName, $fieldType['type'], $reader);
194            }
195
196            return null;
197        }
198
199        return static::deserializeObject($schema, $fieldType, $reader);
200    }
201}