Completed
Push — master ( f594bf...3d13bd )
by brian
01:44
created

Parser::parse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php declare(strict_types=1);
2
3
/**
4
 * @copyright   (c) 2019-present brian ridley
5
 * @author      brian ridley <[email protected]>
6
 * @license     http://opensource.org/licenses/MIT MIT
7
 */
8
9
namespace ptlis\SerializedDataEditor\Parser;
10
11
use ptlis\SerializedDataEditor\Type\ArrayType;
12
use ptlis\SerializedDataEditor\Type\BoolType;
13
use ptlis\SerializedDataEditor\Type\FloatType;
14
use ptlis\SerializedDataEditor\Type\IntegerType;
15
use ptlis\SerializedDataEditor\Type\NullType;
16
use ptlis\SerializedDataEditor\Type\ObjectCustomSerializedType;
17
use ptlis\SerializedDataEditor\Type\ObjectDefaultSerializedType;
18
use ptlis\SerializedDataEditor\Type\ReferenceType;
19
use ptlis\SerializedDataEditor\Type\StringType;
20
use ptlis\SerializedDataEditor\Type\Type;
21
use ptlis\SerializedDataEditor\TypeFragment\ArrayElementIntegerIndex;
22
use ptlis\SerializedDataEditor\TypeFragment\ArrayElementStringIndex;
23
use ptlis\SerializedDataEditor\TypeFragment\ObjectProperty;
24
25
/**
26
 * Parser that parses out a token list into an object graph representing the serialized types and data.
27
 */
28
final class Parser
29
{
30
    /**
31
     * @param Token[] $tokenList
32
     * @return Type
33
     */
34 16
    public function parse(array $tokenList): Type
35
    {
36 16
        return $this->internalParse($tokenList);
37
    }
38
39
    /**
40
     * Internal parse method that tracks the token offset while traversing the token list.
41
     *
42
     * @param Token[] $tokenList
43
     * @param int $tokenOffset Tracks offset when iterating through token list.
44
     * @return Type
45
     */
46 16
    private function internalParse(array $tokenList, int &$tokenOffset = 0): Type
47
    {
48
        // Handle simple (single token) types (e.g. string, integer, etc)
49 16
        if ($this->isSimpleType($tokenList[$tokenOffset])) {
50 15
            $type = $this->parseSimple($tokenList[$tokenOffset], $tokenOffset);
51
52
        // Handle complex types (arrays, objects)
53
        } else {
54 10
            $type = $this->parseComplex($tokenList, $tokenOffset);
55
        }
56
57 16
        return $type;
58
    }
59
60
    /**
61
     * Returns true if the token represents a simple type (consisting of a single token).
62
     */
63 16
    private function isSimpleType(Token $token): bool
64
    {
65 16
        return in_array(
66 16
            $token->getType(),
67
            [
68 16
                Token::NULL,
69
                Token::BOOL,
70
                Token::INTEGER,
71
                Token::FLOAT,
72
                Token::STRING,
73
                Token::REFERENCE
74
            ]
75
        );
76
    }
77
78
    /**
79
     * Parse a simple (single token) type.
80
     *
81
     * @param Token $token
82
     * @param int $tokenOffset Tracks offset when iterating through token list.
83
     * @return Type
84
     */
85 15
    public function parseSimple(Token $token, int &$tokenOffset): Type
86
    {
87 15
        switch ($token->getType()) {
88 15
            case Token::NULL:
89 1
                $type = new NullType();
90 1
                break;
91
92 14
            case Token::BOOL:
93 1
                $type = new BoolType('1' === $token->getValue() ? true : false);
94 1
                break;
95
96 13
            case Token::INTEGER:
97 1
                $type = new IntegerType(intval($token->getValue()));
98 1
                break;
99
100 12
            case Token::FLOAT:
101 1
                $type = new FloatType(floatval($token->getValue()));
102 1
                break;
103
104 11
            case Token::STRING:
105 10
                $type = new StringType($token->getValue());
106 10
                break;
107
108 1
            case Token::REFERENCE:
109 1
                $type = new ReferenceType(intval($token->getValue()));
110 1
                break;
111
        }
112
113 15
        $tokenOffset++;
114
115 15
        return $type;
116
    }
117
118
    /**
119
     * Parse a complex (multi token) type.
120
     *
121
     * @param Token[] $tokenList
122
     * @param int $tokenOffset Tracks offset when iterating through token list.
123
     * @return Type
124
     */
125 10
    private function parseComplex(array $tokenList, int &$tokenOffset): Type
126
    {
127 10
        switch ($tokenList[$tokenOffset]->getType()) {
128 10
            case Token::ARRAY_START:
129 7
                $type = $this->parseArray($tokenList, $tokenOffset);
130 7
                break;
131
132 6
            case Token::OBJECT_DEFAULT_NAME:
133 4
                $type = $this->parseObjectDefaultSerialization($tokenList, $tokenOffset);
134 4
                break;
135
136 2
            case Token::OBJECT_CUSTOM_NAME:
137 2
                $type = $this->parseObjectCustomSerialization($tokenList, $tokenOffset);
138 2
                break;
139
        }
140
141 10
        return $type;
142
    }
143
144
    /**
145
     * Parse an array type.
146
     *
147
     * @param Token[] $tokenList
148
     * @param int $tokenOffset Tracks offset when iterating through token list.
149
     * @return Type
150
     */
151 7
    private function parseArray(array $tokenList, int &$tokenOffset): Type
152
    {
153
        // Skip array open '{'
154 7
        $tokenOffset++;
155
156
        /** @var Token $indexToken */
157 7
        $indexToken = null;
158
159
        // Iterate through tokens array elements
160 7
        $arrayElementList = [];
161 7
        while ($tokenOffset < count($tokenList)) {
162
            switch (true) {
163
                // Do nothing, we're done here
164 7
                case Token::COMPOUND_END === $tokenList[$tokenOffset]->getType():
165 6
                    $tokenOffset++;
166 6
                    break;
167
168
                // Array index, store for use is next iteration
169 7
                case is_null($indexToken):
170 7
                    $indexToken = $tokenList[$tokenOffset];
171 7
                    $tokenOffset++;
172 7
                    break;
173
174
                // Element value, combine with index token to create array element
175 7
                case !is_null($indexToken):
176 7
                    $type = $this->internalParse($tokenList, $tokenOffset);
177
178 7
                    if (Token::INTEGER === $indexToken->getType()) {
179 6
                        $arrayElementList[] = new ArrayElementIntegerIndex(intval($indexToken->getValue()), $type);
180
                    } else {
181 2
                        $arrayElementList[] = new ArrayElementStringIndex($indexToken->getValue(), $type);
182
                    }
183
184 7
                    $indexToken = null;
185 7
                    break;
186
            }
187
        }
188
189 7
        return new ArrayType($arrayElementList);
190
    }
191
192
    /**
193
     * Parse a PHP-serialized object.
194
     *
195
     * @param Token[] $tokenList
196
     * @param int $tokenOffset Tracks offset when iterating through token list.
197
     * @return Type
198
     */
199 4
    public function parseObjectDefaultSerialization(array $tokenList, int &$tokenOffset): Type
200
    {
201 4
        $className = $tokenList[$tokenOffset]->getValue();
202
203
        // Skip object open and property count
204 4
        $tokenOffset += 2;
205
206
        /** @var Token $propertyNameToken */
207 4
        $propertyNameToken = null;
208
209
        // Iterate through tokens building object properties
210 4
        $propertyList = [];
211 4
        while ($tokenOffset < count($tokenList)) {
212
            switch (true) {
213
                // Do nothing, we're done here
214 4
                case Token::COMPOUND_END === $tokenList[$tokenOffset]->getType():
215 3
                    $tokenOffset++;
216 3
                    break;
217
218
                // Property name
219 4
                case is_null($propertyNameToken):
220 4
                    $propertyNameToken = $tokenList[$tokenOffset];
221 4
                    $tokenOffset++;
222 4
                    break;
223
224
                // Property Value
225 4
                case !is_null($propertyNameToken):
226 4
                    $propertyList[] = $this->parseProperty(
227 4
                        $className,
228 4
                        $propertyNameToken,
229 4
                        $this->internalParse($tokenList, $tokenOffset)
230
                    );
231
232 4
                    $propertyNameToken = null;
233 4
                    break;
234
            }
235
        }
236
237 4
        return new ObjectDefaultSerializedType(
238 4
            $className,
239 4
            $propertyList
240
        );
241
    }
242
243
    /**
244
     * Parse a property of a PHP-serialized object.
245
     */
246 4
    private function parseProperty(
247
        string $className,
248
        Token $propertyNameToken,
249
        Type $type
250
    ): ObjectProperty {
251
252
        // Default to public property
253 4
        $visibility = ObjectProperty::PUBLIC;
254 4
        $propertyName = $propertyNameToken->getValue();
255
256
        // Split property name on NUL character
257 4
        $parts = array_values(array_filter(explode("\0", $propertyNameToken->getValue())));
258
259
        // Protected or private property
260 4
        if (count($parts) > 1) {
261 2
            $propertyName = $parts[1];
262 2
            $visibility = ObjectProperty::PROTECTED;
263
264
            // Private property
265 2
            if ($className === $parts[0]) {
266 2
                $visibility = ObjectProperty::PRIVATE;
267
            }
268
        }
269
270 4
        return new ObjectProperty(
271 4
            $visibility,
272 4
            $className,
273 4
            $propertyName,
274 4
            $type
275
        );
276
    }
277
278
    /**
279
     * Parse a custom serialized object.
280
     *
281
     * @param Token[] $tokenList
282
     * @param int $tokenOffset Tracks offset when iterating through token list.
283
     * @return Type
284
     */
285 2
    public function parseObjectCustomSerialization(array $tokenList, int &$tokenOffset): Type
286
    {
287
        // Read class name
288 2
        $className = $tokenList[$tokenOffset]->getValue();
289 2
        $tokenOffset++;
290
291
        // Read data
292 2
        $customSerializedData = $tokenList[$tokenOffset]->getValue();
293 2
        $tokenOffset++;
294
295
        // Skip terminator
296 2
        $tokenOffset++;
297
298 2
        return new ObjectCustomSerializedType($className, $customSerializedData);
299
    }
300
}