Completed
Push — master ( 411d94...8fae9a )
by John
02:59
created

AbstractParser::parse()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
ccs 16
cts 16
cp 1
cc 5
eloc 14
nc 5
nop 1
crap 5
1
<?php
2
/**
3
 * This file is part of graze/unicontroller-client.
4
 *
5
 * Copyright (c) 2016 Nature Delivered Ltd. <https://www.graze.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license https://github.com/graze/unicontroller-client/blob/master/LICENSE.md
11
 * @link https://github.com/graze/unicontroller-client
12
 */
13
namespace Graze\UnicontrollerClient\Parser\Parser;
14
15
use Graze\UnicontrollerClient\Parser\Parser\ParserInterface;
16
use Graze\UnicontrollerClient\Entity\EntityHydrator;
17
use Graze\UnicontrollerClient\Parser\ArrayParser;
18
use Graze\UnicontrollerClient\Parser\BinaryDataParser;
19
20
abstract class AbstractParser implements ParserInterface
21
{
22
    const CONTEXT_NONE = 1;
23
    const CONTEXT_ANSWER = 2;
24
    const CONTEXT_FLUSH = 3;
25
    const CONTEXT_STRING = 4;
26
    const CONTEXT_ARRAY = 5;
27
    const CONTEXT_BINARY = 6;
28
29
    /**
30
     * @var EntityHydrator
31
     */
32
    private $entityHydrator;
33
34
    /**
35
     * @var ArrayParser
36
     */
37
    private $arrayParser;
38
39
    /**
40
     * @var BinaryDataParser
41
     */
42
    private $binaryDataParser;
43
44
    /**
45
     * @var []
46
     */
47
    private $contextCurrentToTokensToContextNew = [
48
        self::CONTEXT_NONE => [
49
            ',' => self::CONTEXT_FLUSH,
50
            '=' => self::CONTEXT_ANSWER,
51
            "\x02" => self::CONTEXT_STRING,
52
            'BinaryData' => self::CONTEXT_BINARY,
53
            "\x02LineItem\x03" => self::CONTEXT_ARRAY,
54
            "\x02BoxItem\x03" => self::CONTEXT_ARRAY,
55
            "\x02TtfItem\x03" => self::CONTEXT_ARRAY,
56
            "\x02BarcodeItem\x03" => self::CONTEXT_ARRAY,
57
            "\x02PictureItem\x03" => self::CONTEXT_ARRAY,
58
            "\x02VarPrompt\x03" => self::CONTEXT_ARRAY,
59
            "\x02VarSeq\x03" => self::CONTEXT_ARRAY,
60
            "\x02VarRtc\x03" => self::CONTEXT_ARRAY,
61
            "\x02VarDatabase\x03" => self::CONTEXT_ARRAY,
62
            "\x02VarUserId\x03" => self::CONTEXT_ARRAY,
63
            "\x02VarShiftCode\x03" => self::CONTEXT_ARRAY,
64
            "\x02VarMachineId\x03" => self::CONTEXT_ARRAY,
65
            "\x02VarDatabaseField\x03" => self::CONTEXT_ARRAY,
66
            "\x02VarMacro\x03" => self::CONTEXT_ARRAY,
67
            "\x02VarMacroOutput\x03" => self::CONTEXT_ARRAY,
68
            "\x02VarSerial\x03" => self::CONTEXT_ARRAY,
69
            "\x02SettingsById\x03" => self::CONTEXT_ARRAY,
70
            "\x02ShiftDefinition\x03" => self::CONTEXT_ARRAY,
71
        ],
72
        self::CONTEXT_STRING => [
73
            "\x03" => self::CONTEXT_NONE,
74
        ],
75
        self::CONTEXT_ARRAY => [
76
            "\x02" => self::CONTEXT_STRING,
77
            "\r\n" => self::CONTEXT_NONE,
78
            'BinaryData' => self::CONTEXT_BINARY,
79
        ],
80
        self::CONTEXT_BINARY => [
81
            'BinaryEnd' => self::CONTEXT_NONE,
82
        ]
83
    ];
84
85
    /**
86
     * @var []
87
     */
88
    private $contextStack = [
89
        self::CONTEXT_NONE
90
    ];
91
92
    /**
93
     * @var string
94
     */
95
    private $contextPrevious;
96
97
    /**
98
     * @var string
99
     */
100
    private $buffer = '';
101
102
    /**
103
     * @var bool
104
     */
105
    private $arrayInitialised = false;
106
107
    /**
108
     * @var string
109
     */
110
    private $arrayName;
111
112
    /**
113
     * @var int
114
     */
115
    private $arrayLength;
116
117
    /**
118
     * @var int
119
     */
120
    private $arrayItemCount;
121
122
    /**
123
     * @var []
124
     */
125
    private $propertyValues = [];
126
127
    /**
128
     * @param EntityHydrator $entityHydrator
129
     * @param ArrayParser $arrayParser
130
     * @param BinaryDataParser $binaryDataParser
131
     */
132 19
    public function __construct(EntityHydrator $entityHydrator, ArrayParser $arrayParser, BinaryDataParser $binaryDataParser)
133
    {
134 19
        $this->entityHydrator = $entityHydrator;
135 19
        $this->arrayParser = $arrayParser;
136 19
        $this->binaryDataParser = $binaryDataParser;
137 19
    }
138
139
    /**
140
     * @param string $string
141
     * @return []
0 ignored issues
show
Documentation introduced by
The doc-type [] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
142
     */
143 19
    public function parse($string)
144
    {
145 19
        for ($pointer = 0; $pointer < strlen($string); $pointer++) {
146 19
            $this->buffer .= $string[$pointer];
147
148 19
            while (true) {
149 19
                $contextNext = $this->getContextFromToken($this->buffer);
150 19
                if (!$contextNext) {
151
                    // nothing found in the string, move to next character
152 19
                    break;
153
                }
154
155
                // we found a token, attempt to change context
156 19
                $contextChanged = $this->changeContext($contextNext);
0 ignored issues
show
Bug introduced by
It seems like $contextNext defined by $this->getContextFromToken($this->buffer) on line 149 can also be of type boolean; however, Graze\UnicontrollerClien...Parser::changeContext() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
157 19
                if (!$contextChanged) {
158
                    // no change, move to next character
159 17
                    break;
160
                };
161
162
                // context has changed, repeat this loop in the new context
163 18
            }
164 19
        }
165
166
        // final flush
167 19
        $this->flushBuffer();
168
169 19
        $data = array_combine($this->getProperties(), $this->propertyValues);
170 19
        $this->propertyValues = [];
171
172 19
        return $this->entityHydrator->hydrate($this->getEntity(), $data);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->entityHydr...s->getEntity(), $data); (Graze\UnicontrollerClien...\Entity\EntityInterface) is incompatible with the return type declared by the interface Graze\UnicontrollerClien...\ParserInterface::parse of type Graze\UnicontrollerClien...\Entity\EntityInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
173
    }
174
175
    /**
176
     * @param string $string
177
     * @return string|bool
178
     */
179 19
    private function getContextFromToken($string)
180
    {
181 19
        $tokensToContextNew = $this->contextCurrentToTokensToContextNew[$this->getContextCurrent()];
182 19
        foreach ($tokensToContextNew as $token => $contextNew) {
183 19
            $offset = strlen($token) * -1;
184 19
            $subject = substr($string, $offset);
185
186 19
            if ($subject != $token) {
187 19
                continue;
188
            }
189
190 19
            return $contextNew;
191 19
        }
192
193 19
        return false;
194
    }
195
196
    /**
197
     * @return string
198
     */
199 19
    private function getContextCurrent()
200
    {
201 19
        return end($this->contextStack);
202
    }
203
204
    /**
205
     * @param string $context
206
     * @return bool
207
     */
208 19
    private function changeContext($context)
209
    {
210 19
        if ($context == self::CONTEXT_ANSWER) {
211
            $this->buffer = ''; // remove 'answerName='
212
            return false;
213
        }
214
215 19
        if ($context == self::CONTEXT_FLUSH) {
216 17
            $this->flushBuffer();
217 17
            $this->contextPrevious = self::CONTEXT_NONE;
0 ignored issues
show
Documentation Bug introduced by
The property $contextPrevious was declared of type string, but self::CONTEXT_NONE is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
218 17
            return false;
219
        }
220
221 18
        if ($context != self::CONTEXT_NONE) {
222
            // enter new context
223 18
            $this->contextStack[] = $context;
224 18
            return true;
225
        }
226
227 18
        if ($this->getContextCurrent() == self::CONTEXT_ARRAY) {
228 2
            if (!$this->arrayInitialised) {
229
                // array not yet initialised
230 2
                $this->arrayInit();
231 2
            }
232
233 2
            if ($this->arrayItemCount > 0) {
234 2
                $this->arrayItemCount--;
235
                // not enough items, do not leave context yet
236 2
                return false;
237
            }
238 2
        }
239
240
        // exit current context
241 18
        $this->contextPrevious = array_pop($this->contextStack);
242 18
        return true;
243
    }
244
245 19
    private function flushBuffer()
246
    {
247 19
        switch ($this->contextPrevious) {
248 19
            case self::CONTEXT_ARRAY:
249 2
                $propertyValue = $this->arrayParser->parse($this->arrayName, $this->arrayLength, $this->buffer);
250 2
                $this->arrayClear();
251 2
                break;
252
253 19
            case self::CONTEXT_BINARY:
254 2
                if ($this->getContextCurrent() != self::CONTEXT_NONE) {
255
                    break;
256
                }
257
258 2
                $propertyValue = $this->binaryDataParser->parse($this->buffer);
259 2
                break;
260
261 19
            default:
262 19
                $propertyValue = trim($this->buffer, "\x02\x03,");
263 19
        }
264
265 19
        $this->propertyValues[] = $propertyValue;
0 ignored issues
show
Bug introduced by
The variable $propertyValue does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
266 19
        $this->buffer = '';
267 19
    }
268
269 2
    private function arrayInit()
270
    {
271 2
        list($name, $length) = explode(',', $this->buffer, 2);
272
273 2
        $this->arrayName = trim($name, "\x02\x03");
274 2
        $this->arrayLength = trim($length, ",\r\n");
0 ignored issues
show
Documentation Bug introduced by
The property $arrayLength was declared of type integer, but trim($length, ', ') is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
275 2
        $this->arrayItemCount = $this->arrayLength;
0 ignored issues
show
Documentation Bug introduced by
The property $arrayItemCount was declared of type integer, but $this->arrayLength is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
276 2
        $this->arrayInitialised = true;
277
278 2
        $this->buffer = '';
279 2
    }
280
281 2
    private function arrayClear()
282
    {
283 2
        $this->arrayName = null;
284 2
        $this->arrayLength = 0;
285 2
        $this->arrayItemCount = 0;
286 2
        $this->arrayInitialised = null;
287 2
    }
288
289
    /**
290
     * @return array
291
     */
292
    abstract protected function getProperties();
293
294
    /**
295
     * @return Graze\UnicontrollerClient\Entity\Entity\EntityInterface
296
     */
297
    abstract protected function getEntity();
298
299
    /**
300
     * @return ParserInterface
301
     */
302 19
    public static function factory()
303
    {
304 19
        return new static(
305 19
            new EntityHydrator(),
306 19
            ArrayParser::factory(),
307 19
            new BinaryDataParser()
308 19
        );
309
    }
310
}
311