Passed
Push — master ( 691ef9...ce20c6 )
by Maxim
03:59 queued 01:02
created

Parser::getReflection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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
    /**
89
     * Returns property's alias.
90
     *
91
     * @return string property's alias
92
     */
93
    public function getAlias(): string
94
    {
95
        return $this->alias;
96
    }
97
98
    /**
99
     * Generates list of methods for property.
100
     *
101
     * @param array $typeTree type tree
102
     * 
103
     * @return string[] methods' names
104
     */
105
    public function processMethods(array $typeTree): array
106
    {
107
        $methods = [];
108
        $name = $this->alias ?? $this->reflection->name;
109
110
        if (isset($this->accessModifiers['read'])) {
111
            $methods[$this->accessModifiers['read']][] = 'get' . ucfirst($name);
112
        }
113
        if (isset($this->accessModifiers['write'])) {
114
            $methods[$this->accessModifiers['write']][] = 'set' . ucfirst($name);
115
        }
116
117
        foreach ($typeTree as $index => $type) {
118
            $class = is_int($index) ? $type : $index;
119
            try {
120
                class_exists($class);
121
            } catch (InternalError $error) {
122
                continue;
123
            }
124
            foreach ((new \ReflectionClass($class))->getMethods() as $method) {
125
                $isAccessible = $method->isStatic() && $method->isPublic() && !$method->isAbstract();
126
                if ($isAccessible && preg_match('{^m_(in|out)_.*?PROPERTY.*}', $method->name)) {
127
                    if (substr($method->name, 0, 5) == 'm_in_' && isset($this->accessModifiers['write'])) {
128
                        $methods[$this->accessModifiers['write']][] = str_replace('PROPERTY', ucfirst($name),
129
                            substr($method->name, 5));
130
                    } elseif (substr($method->name, 0, 6) == 'm_out_' && isset($this->accessModifiers['read'])) {
131
                        $methods[$this->accessModifiers['read']][] = str_replace('PROPERTY', ucfirst($name),
132
                            substr($method->name, 6));
133
                    }
134
                }
135
            }
136
        }
137
        return $methods;
138
    }
139
140
    /**
141
     * Creates list of handlers for input data.
142
     *
143
     * @return string[] handlers
144
     */
145
    public function processInputHandlers(): array
146
    {
147
        return $this->processTokens(!$this->readableFirst, self::HANDLERS_1, self::HANDLERS_2,
148
            [$this, 'makeHandlersList']);
149
    }
150
151
    /**
152
     * Creates list of handlers for output data.
153
     *
154
     * @return string[] handlers
155
     */
156
    public function processOutputHandlers(): array
157
    {
158
        return $this->processTokens($this->readableFirst, self::HANDLERS_1, self::HANDLERS_2,
159
            [$this, 'makeHandlersList']);
160
    }
161
162
    /**
163
     * Creates list of conditions for input data.
164
     *
165
     * @return string[] conditions
166
     */
167
    public function processInputConditions(): array
168
    {
169
        return $this->processConditions(!$this->readableFirst);
170
    }
171
172
    /**
173
     * Creates list of conditions for output data.
174
     *
175
     * @return string[] conditions
176
     */
177
    public function processOutputConditions(): array
178
    {
179
        return $this->processConditions($this->readableFirst);
180
    }
181
182
    /**
183
     * Processes access modifiers for getter and setter.
184
     *
185
     * @return string[] access modifiers
186
     */
187
    public function processAccessModifier(): array
188
    {
189
        $type = $this->getKeyword(self::KEYWORD_1);
190
        if ($type == 'access') {
191
            $this->accessModifiers = [
192
                'write' => $this->replaceSignWithWord($this->tokens[self::ACCESS_MODIFIER_1]),
193
                'read' => $this->replaceSignWithWord($this->tokens[self::ACCESS_MODIFIER_1])
194
            ];
195
            return $this->accessModifiers;
196
        }
197
        $this->accessModifiers[$type] = $this->replaceSignWithWord($this->tokens[self::ACCESS_MODIFIER_1]);
198
        if (isset($this->tokens[self::KEYWORD_2])) {
199
            $type = $this->getKeyword(self::KEYWORD_2);
200
            $this->accessModifiers[$type] = $this->replaceSignWithWord($this->tokens[self::ACCESS_MODIFIER_2]);
201
        }
202
        return $this->accessModifiers;
203
    }
204
205
    /**
206
     * Processes property's alias.
207
     */
208
    private function processAlias(): void
209
    {
210
        $this->alias = $this->tokens[self::ALIAS] ?? $this->reflection->name;
211
    }
212
213
    /**
214
     * Turns short style of access modifier to the full keyword.
215
     *
216
     * @param string $sign access modifier sign
217
     * @return string access modifier
218
     * @throws InternalError if access modifier is invalid
219
     */
220
    private function replaceSignWithWord(string $sign): string
221
    {
222
        switch ($sign) {
223
            case self::A_PUBLIC:
224
                return 'public';
225
            case self::A_PROTECTED:
226
                return 'protected';
227
            case self::A_PRIVATE:
228
                return 'private';
229
            default:
230
                throw new InternalError('not a valid access modifier given');
231
        }
232
    }
233
234
    /**
235
     * Creates list of handlers from a string of handlers definition.
236
     *
237
     * @param string $handlers handlers
238
     * @return string[] handlers
239
     */
240
    private function makeHandlersList(string $handlers): array
241
    {
242
        $result = preg_replace_callback(
243
            '{`([^`]|\\\\`)+((?<!\\\\)`)}',
244
            function (array $matches) {
245
                return addcslashes($matches[0], ',');
246
            },
247
            $handlers
248
        );
249
        $result = preg_split('{(?<!\\\\),\s*}', $result);
250
        foreach ($result as &$handler) {
251
            $handler = $this->resolveClassNames(stripcslashes($handler));
252
        }
253
        return $result;
254
    }
255
256
    /**
257
     * Creates list of conditions from a string of conditions definition.
258
     *
259
     * @param string $conditions conditions
260
     * @return array conditions
261
     */
262
    private function explodeConditions(string $conditions): array
263
    {
264
        $result = [];
265
        $conditions = preg_replace_callback(
266
            '{`([^`]|\\\\`)+((?<!\\\\)`)}',
267
            function (array $matches) {
268
                return addcslashes($matches[0], '&|');
269
            },
270
            $conditions
271
        );
272
        $conditions = preg_split('{\s*\|\|\s*}', $conditions);
273
        foreach ($conditions as $condition) {
274
            $result[] = preg_split('{\s*&&\s*}', $condition);
275
        }
276
        foreach ($result as $number => &$complexCondition) {
277
            if (is_array($complexCondition)) {
278
                foreach ($complexCondition as $num => &$condition) {
279
                    $condition = stripcslashes($condition);
280
                }
281
            } else {
282
                $complexCondition = stripcslashes($complexCondition);
283
            }
284
        }
285
        return $result;
286
    }
287
288
    /**
289
     * Processes tokens.
290
     *
291
     * @param bool $mode a flag; mode of execution
292
     * @param int $token1 first token
293
     * @param int $token2 second token
294
     * @param callable $callback special callback
295
     * @return string[] normalized array of Axessors tokens
296
     */
297
    private function processTokens(bool $mode, int $token1, int $token2, callable $callback): array
298
    {
299
        if ($mode && isset($this->tokens[$token1])) {
300
            return $callback($this->tokens[$token1]);
301
        } elseif (!$mode && isset($this->tokens[$token2])) {
302
            return $callback($this->tokens[$token2]);
303
        } else {
304
            return [];
305
        }
306
    }
307
308
    /**
309
     * Processes conditions.
310
     *
311
     * @param bool $mode mode of execution
312
     * @return string[] conditions
313
     */
314
    private function processConditions(bool $mode): array
315
    {
316
        return $this->processTokens($mode, self::CONDITIONS_1, self::CONDITIONS_2, [$this, 'makeConditionsTree']);
317
    }
318
319
    /**
320
     * Makes tree of conditions.
321
     *
322
     * @param string $conditions string with conditions definition
323
     * @return array tree of conditions
324
     */
325
    private function makeConditionsTree(string $conditions): array
326
    {
327
        $result = [];
328
        $conditions = $this->explodeConditions($conditions);
329
        foreach ($conditions as $number => $condition) {
330
            foreach ($condition as $token) {
331
                if (count($condition) === 1) {
332
                    $result[] = $this->resolveClassNames($token);
333
                } else {
334
                    $result[$number][] = $token;
335
                }
336
            }
337
        }
338
        return $result;
339
    }
340
341
    /**
342
     * Validates order of statements in Axessors comment.
343
     *
344
     * @throws SyntaxError if the statements go in incorrect order
345
     */
346
    private function validateStatements(): void
347
    {
348
        if (isset($this->tokens[self::KEYWORD_2])) {
349
            if ($this->tokens[self::KEYWORD_1] == $this->tokens[self::KEYWORD_2]) {
350
                throw new SyntaxError("the same statements in {$this->reflection->getDeclaringClass()->name}::\${$this->reflection->name} Axessors comment");
351
            } elseif (preg_match('{^(wrt|writable)$}', $this->tokens[self::KEYWORD_2])) {
352
                throw new SyntaxError(
353
                    "\"writable\" statement must be the first in {$this->reflection->getDeclaringClass()->name}::\${$this->reflection->name} Axessors comment\""
354
                );
355
            }
356
        }
357
    }
358
359
    /**
360
     * Returns normalized keyword with type of access.
361
     *
362
     * @param int $token token
363
     * @return string keyword
364
     * @throws InternalError if the token with keyword is not valid
365
     */
366
    private function getKeyword(int $token): string
367
    {
368
        if (preg_match(sprintf('{^(%s|%s)$}', self::F_ACCESSIBLE, self::S_ACCESSIBLE), $this->tokens[$token])) {
369
            return 'access';
370
        } elseif (preg_match(sprintf('{^(%s|%s)$}', self::F_WRITABLE, self::S_WRITABLE), $this->tokens[$token])) {
371
            return 'write';
372
        } elseif (preg_match(sprintf('{^(%s|%s)$}', self::F_READABLE, self::S_READABLE), $this->tokens[$token])) {
373
            return 'read';
374
        } else {
375
            throw new InternalError('not a valid keyword token given');
376
        }
377
    }
378
379
    /**
380
     * Resolves class names in *injected* callbacks and conditions.
381
     *
382
     * @param string $expression executable string
383
     * @return string expression with resolved class names
384
     */
385
    private function resolveClassNames(string $expression): string
386
    {
387
        $expression = preg_replace_callback('/"[^"]"|\'[^\']\'/', function (array $matches) {
388
            return str_replace(':', ':\\', $matches[0]);
389
        }, $expression);
390
        $expression = preg_replace('/(?<!:):(?=([a-zA-Z_][a-zA-Z0-9_]*))/', "$this->namespace\\", $expression);
391
        $expression = preg_replace_callback('/"[^"]"|\'[^\']\'/', function (array $matches) {
392
            return str_replace(':\\', ':', $matches[0]);
393
        }, $expression);
394
        return $expression;
395
    }
396
}
397