Passed
Push — master ( ce20c6...df670c )
by Maxim
02:56
created

Parser::explodeConditions()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 6
nop 1
dl 0
loc 24
ccs 0
cts 17
cp 0
crap 30
rs 8.5125
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Parser::validateStatements() 0 8 4
1
<?php
2
/**
3
 * This file is a part of "Axessors" library.
4
 *
5
 * @author <[email protected]>
6
 * @license GPL
7
 */
8
9
namespace NoOne4rever\Axessors;
10
11
use NoOne4rever\Axessors\Exceptions\InternalError;
12
use NoOne4rever\Axessors\Exceptions\SyntaxError;
13
14
/**
15
 * Class Parser.
16
 *
17
 * Analyses tokens from the Axessors comment.
18
 *
19
 * @package NoOne4rever\Axessors
20
 */
21
class Parser
22
{
23
    private const ACCESS_MODIFIER_1 = 1;
24
    private const KEYWORD_1 = 2;
25
    private const TYPE = 3;
26
    private const CONDITIONS_1 = 4;
27
    private const HANDLERS_1 = 6;
28
    private const ACCESS_MODIFIER_2 = 7;
29
    private const KEYWORD_2 = 8;
30
    private const CONDITIONS_2 = 9;
31
    private const HANDLERS_2 = 11;
32
    private const ALIAS = 13;
33
34
    private const F_WRITABLE = 'writable';
35
    private const S_WRITABLE = 'wrt';
36
    private const F_ACCESSIBLE = 'accessible';
37
    private const S_ACCESSIBLE = 'axs';
38
    private const F_READABLE = 'readable';
39
    private const S_READABLE = 'rdb';
40
    private const A_PUBLIC = '+';
41
    private const A_PROTECTED = '~';
42
    private const A_PRIVATE = '-';
43
44
    /** @var string[] tokens from Axessors comment */
45
    private $tokens;
46
    /** @var \ReflectionProperty property's reflection */
47
    private $reflection;
48
    /** @var string[] access modifiers for getter and setter */
49
    private $accessModifiers;
50
    /** @var string alias of property */
51
    private $alias;
52
    /** @var string class' namespace */
53
    private $namespace;
54
    /** @var bool information about order of tokens */
55
    private $readableFirst;
56
57
    /**
58
     * Parser constructor.
59
     *
60
     * @param \ReflectionProperty $reflection property's reflection
61
     * @param array $tokens tokens from the Axessors comment
62
     */
63
    public function __construct(\ReflectionProperty $reflection, array $tokens)
64
    {
65
        $this->reflection = $reflection;
66
        $this->tokens = $tokens;
67
        $this->namespace = $reflection->getDeclaringClass()->getNamespaceName();
68
        $this->readableFirst = (bool)preg_match('{^(rdb|readable)$}', $this->tokens[self::KEYWORD_1]);
69
        $this->validateStatements();
70
        $this->processAlias();
71
    }
72
    
73
    public function getTypeDef(): string 
74
    {
75
        return $this->tokens[self::TYPE];
76
    }
77
    
78
    public function getNamespace(): string 
79
    {
80
        return $this->namespace;
81
    }
82
    
83
    public function getReflection(): \ReflectionProperty
84
    {
85
        return $this->reflection;
86
    }
87
88
    public function getInConditions(): string 
89
    {
90
        return $this->tokens[$this->readableFirst ? self::CONDITIONS_2 : self::CONDITIONS_1] ?? '';
91
    }
92
    
93
    public function getOutConditions(): string 
94
    {
95
        return $this->tokens[$this->readableFirst ? self::CONDITIONS_1 : self::CONDITIONS_2] ?? '';
96
    }
97
    
98
    /**
99
     * Returns property's alias.
100
     *
101
     * @return string property's alias
102
     */
103
    public function getAlias(): string
104
    {
105
        return $this->alias;
106
    }
107
108
    /**
109
     * Generates list of methods for property.
110
     *
111
     * @param array $typeTree type tree
112
     * 
113
     * @return string[] methods' names
114
     */
115
    public function processMethods(array $typeTree): array
116
    {
117
        $methods = [];
118
        $name = $this->alias ?? $this->reflection->name;
119
120
        if (isset($this->accessModifiers['read'])) {
121
            $methods[$this->accessModifiers['read']][] = 'get' . ucfirst($name);
122
        }
123
        if (isset($this->accessModifiers['write'])) {
124
            $methods[$this->accessModifiers['write']][] = 'set' . ucfirst($name);
125
        }
126
127
        foreach ($typeTree as $index => $type) {
128
            $class = is_int($index) ? $type : $index;
129
            try {
130
                class_exists($class);
131
            } catch (InternalError $error) {
132
                continue;
133
            }
134
            foreach ((new \ReflectionClass($class))->getMethods() as $method) {
135
                $isAccessible = $method->isStatic() && $method->isPublic() && !$method->isAbstract();
136
                if ($isAccessible && preg_match('{^m_(in|out)_.*?PROPERTY.*}', $method->name)) {
137
                    if (substr($method->name, 0, 5) == 'm_in_' && isset($this->accessModifiers['write'])) {
138
                        $methods[$this->accessModifiers['write']][] = str_replace('PROPERTY', ucfirst($name),
139
                            substr($method->name, 5));
140
                    } elseif (substr($method->name, 0, 6) == 'm_out_' && isset($this->accessModifiers['read'])) {
141
                        $methods[$this->accessModifiers['read']][] = str_replace('PROPERTY', ucfirst($name),
142
                            substr($method->name, 6));
143
                    }
144
                }
145
            }
146
        }
147
        return $methods;
148
    }
149
150
    /**
151
     * Creates list of handlers for input data.
152
     *
153
     * @return string[] handlers
154
     */
155
    public function processInputHandlers(): array
156
    {
157
        return $this->processTokens(!$this->readableFirst, self::HANDLERS_1, self::HANDLERS_2,
158
            [$this, 'makeHandlersList']);
159
    }
160
161
    /**
162
     * Creates list of handlers for output data.
163
     *
164
     * @return string[] handlers
165
     */
166
    public function processOutputHandlers(): array
167
    {
168
        return $this->processTokens($this->readableFirst, self::HANDLERS_1, self::HANDLERS_2,
169
            [$this, 'makeHandlersList']);
170
    }
171
172
    /**
173
     * Processes access modifiers for getter and setter.
174
     *
175
     * @return string[] access modifiers
176
     */
177
    public function processAccessModifier(): array
178
    {
179
        $type = $this->getKeyword(self::KEYWORD_1);
180
        if ($type == 'access') {
181
            $this->accessModifiers = [
182
                'write' => $this->replaceSignWithWord($this->tokens[self::ACCESS_MODIFIER_1]),
183
                'read' => $this->replaceSignWithWord($this->tokens[self::ACCESS_MODIFIER_1])
184
            ];
185
            return $this->accessModifiers;
186
        }
187
        $this->accessModifiers[$type] = $this->replaceSignWithWord($this->tokens[self::ACCESS_MODIFIER_1]);
188
        if (isset($this->tokens[self::KEYWORD_2])) {
189
            $type = $this->getKeyword(self::KEYWORD_2);
190
            $this->accessModifiers[$type] = $this->replaceSignWithWord($this->tokens[self::ACCESS_MODIFIER_2]);
191
        }
192
        return $this->accessModifiers;
193
    }
194
195
    /**
196
     * Processes property's alias.
197
     */
198
    private function processAlias(): void
199
    {
200
        $this->alias = $this->tokens[self::ALIAS] ?? $this->reflection->name;
201
    }
202
203
    /**
204
     * Turns short style of access modifier to the full keyword.
205
     *
206
     * @param string $sign access modifier sign
207
     * @return string access modifier
208
     * @throws InternalError if access modifier is invalid
209
     */
210
    private function replaceSignWithWord(string $sign): string
211
    {
212
        switch ($sign) {
213
            case self::A_PUBLIC:
214
                return 'public';
215
            case self::A_PROTECTED:
216
                return 'protected';
217
            case self::A_PRIVATE:
218
                return 'private';
219
            default:
220
                throw new InternalError('not a valid access modifier given');
221
        }
222
    }
223
224
    /**
225
     * Creates list of handlers from a string of handlers definition.
226
     *
227
     * @param string $handlers handlers
228
     * @return string[] handlers
229
     */
230
    private function makeHandlersList(string $handlers): array
231
    {
232
        $result = preg_replace_callback(
233
            '{`([^`]|\\\\`)+((?<!\\\\)`)}',
234
            function (array $matches) {
235
                return addcslashes($matches[0], ',');
236
            },
237
            $handlers
238
        );
239
        $result = preg_split('{(?<!\\\\),\s*}', $result);
240
        foreach ($result as &$handler) {
241
            $injProcessor = new InjectedStringParser(stripcslashes($handler));
242
            $handler = $injProcessor->resolveClassNames($this->namespace);
243
        }
244
        return $result;
245
    }
246
247
    /**
248
     * Processes tokens.
249
     *
250
     * @param bool $mode a flag; mode of execution
251
     * @param int $token1 first token
252
     * @param int $token2 second token
253
     * @param callable $callback special callback
254
     * @return string[] normalized array of Axessors tokens
255
     */
256
    private function processTokens(bool $mode, int $token1, int $token2, callable $callback): array
257
    {
258
        if ($mode && isset($this->tokens[$token1])) {
259
            return $callback($this->tokens[$token1]);
260
        } elseif (!$mode && isset($this->tokens[$token2])) {
261
            return $callback($this->tokens[$token2]);
262
        } else {
263
            return [];
264
        }
265
    }
266
267
    /**
268
     * Validates order of statements in Axessors comment.
269
     *
270
     * @throws SyntaxError if the statements go in incorrect order
271
     */
272
    private function validateStatements(): void
273
    {
274
        if (isset($this->tokens[self::KEYWORD_2])) {
275
            if ($this->tokens[self::KEYWORD_1] == $this->tokens[self::KEYWORD_2]) {
276
                throw new SyntaxError("the same statements in {$this->reflection->getDeclaringClass()->name}::\${$this->reflection->name} Axessors comment");
277
            } elseif (preg_match('{^(wrt|writable)$}', $this->tokens[self::KEYWORD_2])) {
278
                throw new SyntaxError(
279
                    "\"writable\" statement must be the first in {$this->reflection->getDeclaringClass()->name}::\${$this->reflection->name} Axessors comment\""
280
                );
281
            }
282
        }
283
    }
284
285
    /**
286
     * Returns normalized keyword with type of access.
287
     *
288
     * @param int $token token
289
     * @return string keyword
290
     * @throws InternalError if the token with keyword is not valid
291
     */
292
    private function getKeyword(int $token): string
293
    {
294
        if (preg_match(sprintf('{^(%s|%s)$}', self::F_ACCESSIBLE, self::S_ACCESSIBLE), $this->tokens[$token])) {
295
            return 'access';
296
        } elseif (preg_match(sprintf('{^(%s|%s)$}', self::F_WRITABLE, self::S_WRITABLE), $this->tokens[$token])) {
297
            return 'write';
298
        } elseif (preg_match(sprintf('{^(%s|%s)$}', self::F_READABLE, self::S_READABLE), $this->tokens[$token])) {
299
            return 'read';
300
        } else {
301
            throw new InternalError('not a valid keyword token given');
302
        }
303
    }
304
}
305