Completed
Push — master ( 345204...d371ed )
by Edward
04:50
created

CallbackBuilder::getVarName()   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
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace Remorhaz\JSON\Path\Query;
5
6
use Remorhaz\JSON\Path\Runtime\LiteralFactoryInterface;
7
use Remorhaz\JSON\Path\Runtime\Matcher\MatcherFactoryInterface;
8
use Remorhaz\JSON\Path\Runtime\ValueListFetcherInterface;
9
use Remorhaz\JSON\Path\Value\NodeValueListInterface;
10
use function array_map;
11
use function array_reverse;
12
use PhpParser\BuilderFactory;
13
use PhpParser\Node as PhpAstNode;
14
use PhpParser\Node\Arg;
15
use PhpParser\Node\Expr;
16
use PhpParser\Node\Expr\Assign;
17
use PhpParser\Node\Stmt\Expression;
18
use PhpParser\Node\Stmt\Return_;
19
use PhpParser\PrettyPrinter\Standard;
20
use Remorhaz\JSON\Path\Runtime\EvaluatorInterface;
21
use Remorhaz\JSON\Path\Value\ValueListInterface;
22
use Remorhaz\UniLex\AST\AbstractTranslatorListener;
23
use Remorhaz\UniLex\AST\Node as QueryAstNode;
24
use Remorhaz\UniLex\Exception as UniLexException;
25
use Remorhaz\UniLex\Stack\PushInterface;
26
use function is_callable;
27
28
final class CallbackBuilder extends AbstractTranslatorListener implements CallbackBuilderInterface
29
{
30
31
    private const ARG_INPUT = 'input';
32
33
    private const ARG_VALUE_LIST_FETCHER = 'valueListFetcher';
34
35
    private const ARG_EVALUATOR = 'evaluator';
36
37
    private const ARG_LITERAL_FACTORY = 'literalFactory';
38
39
    private const ARG_MATCHER_FACTORY = 'matcherFactory';
40
41
    private $php;
42
43
    private $references = [];
44
45
    private $input;
46
47
    private $valueListFetcher;
48
49
    private $evaluator;
50
51
    private $literalFactory;
52
53
    private $matcherFactory;
54
55
    private $stmts = [];
56
57
    private $callback;
58
59
    private $callbackCode;
60
61
    private $capabilities;
62
63 6
    public function __construct()
64
    {
65 6
        $this->php = new BuilderFactory;
66 6
    }
67
68
    public function getCallback(): callable
69
    {
70
        if (!isset($this->callback)) {
71
            $callback = eval($this->getCallbackCode());
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
72
            if (!is_callable($callback)) {
73
                throw new Exception\InvalidCallbackCodeException($this->getCallbackCode());
74
            }
75
            $this->callback = $callback;
76
        }
77
78
        return $this->callback;
79
    }
80
81 1
    public function getCallbackCode(): string
82
    {
83 1
        if (isset($this->callbackCode)) {
84
            return $this->callbackCode;
85
        }
86
87 1
        throw new Exception\QueryCallbackCodeNotFoundException;
88
    }
89
90 3
    public function getCapabilities(): CapabilitiesInterface
91
    {
92 3
        if (isset($this->capabilities)) {
93 2
            return $this->capabilities;
94
        }
95
96 1
        throw new Exception\CapabilitiesNotFoundException;
97
    }
98
99 3
    public function onStart(QueryAstNode $node): void
100
    {
101 3
        $this->input = $this->php->var(self::ARG_INPUT);
102 3
        $this->valueListFetcher = $this->php->var(self::ARG_VALUE_LIST_FETCHER);
103 3
        $this->evaluator = $this->php->var(self::ARG_EVALUATOR);
104 3
        $this->literalFactory = $this->php->var(self::ARG_LITERAL_FACTORY);
105 3
        $this->matcherFactory = $this->php->var(self::ARG_MATCHER_FACTORY);
106
107 3
        $this->stmts = [];
108 3
        $this->references = [];
109 3
        unset($this->callback, $this->callbackCode, $this->capabilities);
110 3
    }
111
112
    public function onFinish(): void
113
    {
114
        $inputParam = $this
115
            ->php
116
            ->param(self::ARG_INPUT)
117
            ->setType(NodeValueListInterface::class)
118
            ->getNode();
119
        $valueListFetcherParam = $this
120
            ->php
121
            ->param(self::ARG_VALUE_LIST_FETCHER)
122
            ->setType(ValueListFetcherInterface::class)
123
            ->getNode();
124
        $evaluatorParam = $this
125
            ->php
126
            ->param(self::ARG_EVALUATOR)
127
            ->setType(EvaluatorInterface::class)
128
            ->getNode();
129
        $literalFactoryParam = $this
130
            ->php
131
            ->param(self::ARG_LITERAL_FACTORY)
132
            ->setType(LiteralFactoryInterface::class)
133
            ->getNode();
134
        $matcherFactoryParam = $this
135
            ->php
136
            ->param(self::ARG_MATCHER_FACTORY)
137
            ->setType(MatcherFactoryInterface::class)
138
            ->getNode();
139
        $stmts = array_map(
140
            function (PhpAstNode $stmt): PhpAstNode {
141
                return $stmt instanceof Expr ? new Expression($stmt): $stmt;
142
            },
143
            $this->stmts
144
        );
145
146
        $closure = new Expr\Closure(
147
            [
148
                'stmts' => $stmts,
149
                'returnType' => ValueListInterface::class,
150
                'params' => [
151
                    $inputParam,
152
                    $valueListFetcherParam,
153
                    $evaluatorParam,
154
                    $literalFactoryParam,
155
                    $matcherFactoryParam
156
                ],
157
            ]
158
        );
159
        $return = new Return_($closure);
160
161
        $this->callbackCode = (new Standard)->prettyPrint([$return]);
162
    }
163
164 1
    public function onBeginProduction(QueryAstNode $node, PushInterface $stack): void
165
    {
166 1
        $stack->push(...array_reverse($node->getChildList()));
167 1
    }
168
169
    /**
170
     * @param QueryAstNode $node
171
     * @throws UniLexException
172
     */
173 3
    public function onFinishProduction(QueryAstNode $node): void
174
    {
175 3
        if ($this->hasReference($node)) {
176
            return;
177
        }
178 3
        switch ($node->getName()) {
179
            case AstNodeType::GET_INPUT:
180 2
                $this->setReference($node, $this->input);
181 2
                break;
182
183
            case AstNodeType::SET_OUTPUT:
184 3
                $this->capabilities = new Capabilities(
185 3
                    $node->getAttribute('is_definite'),
186 3
                    $node->getAttribute('is_addressable'),
187
                );
188 3
                $this->stmts[] = new Return_($this->getReference($node->getChild(0)));
189 2
                break;
190
191
            case AstNodeType::FETCH_CHILDREN:
192
                /** @see ValueListFetcherInterface::fetchChildren() */
193
                $this->addMethodCall(
194
                    $node,
195
                    $this->valueListFetcher,
196
                    'fetchChildren',
197
                    $this->getReference($node->getChild(0)),
198
                    $this->getReference($node->getChild(1)),
199
                );
200
                break;
201
202
            case AstNodeType::FETCH_CHILDREN_DEEP:
203
                /** @see ValueListFetcherInterface::fetchChildrenDeep() */
204
                $this->addMethodCall(
205
                    $node,
206
                    $this->valueListFetcher,
207
                    'fetchChildrenDeep',
208
                    $this->getReference($node->getChild(0)),
209
                    $this->getReference($node->getChild(1)),
210
                );
211
                break;
212
213
            case AstNodeType::FETCH_FILTER_CONTEXT:
214
                /** @see ValueListFetcherInterface::fetchFilterContext() */
215
                $this->addMethodCall(
216
                    $node,
217
                    $this->valueListFetcher,
218
                    'fetchFilterContext',
219
                    $this->getReference($node->getChild(0)),
220
                );
221
                break;
222
223
            case AstNodeType::SPLIT_FILTER_CONTEXT:
224
                /** @see ValueListFetcherInterface::splitFilterContext() */
225
                $this->addMethodCall(
226
                    $node,
227
                    $this->valueListFetcher,
228
                    'splitFilterContext',
229
                    $this->getReference($node->getChild(0)),
230
                );
231
                break;
232
233
            case AstNodeType::JOIN_FILTER_RESULTS:
234
                /** @see ValueListFetcherInterface::joinFilterResults() */
235
                $this->addMethodCall(
236
                    $node,
237
                    $this->valueListFetcher,
238
                    'joinFilterResults',
239
                    $this->getReference($node->getChild(0)),
240
                    $this->getReference($node->getChild(1)),
241
                );
242
                break;
243
244
            case AstNodeType::FILTER:
245
                /** @see ValueListFetcherInterface::fetchFilteredValues() */
246
                $this->addMethodCall(
247
                    $node,
248
                    $this->valueListFetcher,
249
                    'fetchFilteredValues',
250
                    $this->getReference($node->getChild(0)),
251
                    $this->getReference($node->getChild(1)),
252
                );
253
                break;
254
255
            case AstNodeType::EVALUATE:
256
                /** @see EvaluatorInterface::evaluate() */
257
                $this->addMethodCall(
258
                    $node,
259
                    $this->evaluator,
260
                    'evaluate',
261
                    $this->getReference($node->getChild(0)),
262
                    $this->getReference($node->getChild(1)),
263
                );
264
                break;
265
266
            case AstNodeType::EVALUATE_LOGICAL_OR:
267
                /** @see EvaluatorInterface::logicalOr() */
268
                $this->addMethodCall(
269
                    $node,
270
                    $this->evaluator,
271
                    'logicalOr',
272
                    $this->getReference($node->getChild(0)),
273
                    $this->getReference($node->getChild(1)),
274
                );
275
                break;
276
277
            case AstNodeType::EVALUATE_LOGICAL_AND:
278
                /** @see EvaluatorInterface::logicalAnd() */
279
                $this->addMethodCall(
280
                    $node,
281
                    $this->evaluator,
282
                    'logicalAnd',
283
                    $this->getReference($node->getChild(0)),
284
                    $this->getReference($node->getChild(1)),
285
                );
286
                break;
287
288
            case AstNodeType::EVALUATE_LOGICAL_NOT:
289
                /** @see EvaluatorInterface::logicalNot() */
290
                $this->addMethodCall(
291
                    $node,
292
                    $this->evaluator,
293
                    'logicalNot',
294
                    $this->getReference($node->getChild(0)),
295
                );
296
                break;
297
298
            case AstNodeType::CALCULATE_IS_EQUAL:
299
                /** @see EvaluatorInterface::isEqual() */
300
                $this->addMethodCall(
301
                    $node,
302
                    $this->evaluator,
303
                    'isEqual',
304
                    $this->getReference($node->getChild(0)),
305
                    $this->getReference($node->getChild(1)),
306
                );
307
                break;
308
309
            case AstNodeType::CALCULATE_IS_GREATER:
310
                /** @see EvaluatorInterface::isGreater() */
311
                $this->addMethodCall(
312
                    $node,
313
                    $this->evaluator,
314
                    'isGreater',
315
                    $this->getReference($node->getChild(0)),
316
                    $this->getReference($node->getChild(1)),
317
                );
318
                break;
319
320
            case AstNodeType::CALCULATE_IS_REGEXP:
321
                /** @see EvaluatorInterface::isRegExp() */
322
                $this->addMethodCall(
323
                    $node,
324
                    $this->evaluator,
325
                    'isRegExp',
326
                    $this->php->val($node->getAttribute('pattern')),
327
                    $this->getReference($node->getChild(0)),
328
                );
329
                break;
330
331
            case AstNodeType::MATCH_ANY_CHILD:
332
                /** @see MatcherFactoryInterface::matchAnyChild() */
333
                $this->addMethodCall(
334
                    $node,
335
                    $this->matcherFactory,
336
                    'matchAnyChild',
337
                );
338
                break;
339
340
            case AstNodeType::MATCH_PROPERTY_STRICTLY:
341
                /** @see MatcherFactoryInterface::matchPropertyStrictly() */
342
                $this->addMethodCall(
343
                    $node,
344
                    $this->matcherFactory,
345
                    'matchPropertyStrictly',
346
                    ...array_map(
347
                        [$this->php, 'val'],
348
                        $node->getAttribute('names')
349
                    ),
350
                );
351
                break;
352
353
            case AstNodeType::MATCH_ELEMENT_STRICTLY:
354
                /** @see MatcherFactoryInterface::matchElementStrictly() */
355
                $this->addMethodCall(
356
                    $node,
357
                    $this->matcherFactory,
358
                    'matchElementStrictly',
359
                    ...array_map(
360
                        [$this->php, 'val'],
361
                        $node->getAttribute('indexes')
362
                    ),
363
                );
364
                break;
365
366
            case AstNodeType::MATCH_ELEMENT_SLICE:
367
                /** @see MatcherFactoryInterface::matchElementSlice() */
368
                $this->addMethodCall(
369
                    $node,
370
                    $this->matcherFactory,
371
                    'matchElementSlice',
372
                    $this
373
                        ->php
374
                        ->val($node->getAttribute('hasStart') ? $node->getAttribute('start') : null),
375
                    $this
376
                        ->php
377
                        ->val($node->getAttribute('hasEnd') ? $node->getAttribute('end') : null),
378
                    $this
379
                        ->php
380
                        ->val($node->getAttribute('step')),
381
                );
382
                break;
383
384
            case AstNodeType::AGGREGATE:
385
                /** @see EvaluatorInterface::aggregate() */
386
                $this->addMethodCall(
387
                    $node,
388
                    $this->evaluator,
389
                    'aggregate',
390
                    $this->php->val($node->getAttribute('name')),
391
                    $this->getReference($node->getChild(0)),
392
                );
393
                break;
394
395
            case AstNodeType::CREATE_LITERAL_SCALAR:
396
                $attributes = $node->getAttributeList();
397
                $value = $attributes['value'] ?? null; // TODO: allow pass null in attribute
398
                /** @see LiteralFactoryInterface::createScalar() */
399
                $this->addMethodCall(
400
                    $node,
401
                    $this->literalFactory,
402
                    'createScalar',
403
                    $this->getReference($node->getChild(0)),
404
                    $this->php->val($value),
405
                );
406
                break;
407
408
            case AstNodeType::CREATE_LITERAL_ARRAY:
409
                /** @see LiteralFactoryInterface::createArray() */
410
                $this->addMethodCall(
411
                    $node,
412
                    $this->literalFactory,
413
                    'createArray',
414
                    $this->getReference($node->getChild(0)),
415
                    new Arg(
416
                        $this->getReference($node->getChild(1)),
417
                        false,
418
                        true,
419
                    ),
420
                );
421
                break;
422
423
            case AstNodeType::CREATE_ARRAY:
424
                // [ X:APPEND_TO_ARRAY ]
425
                $items = [];
426
                foreach ($node->getChildList() as $child) {
427
                    $items[] = $this->getReference($child);
428
                }
429
                $this->stmts[] = new Assign(
430
                    $this->createReference($node),
431
                    $this->php->val($items),
432
                );
433
                break;
434
435
            case AstNodeType::APPEND_TO_ARRAY:
436
                // [ 0:<value> ]
437
                $this->setReference(
438
                    $node,
439
                    $this->getReference($node->getChild(0)),
440
                );
441
                break;
442
        }
443 2
    }
444
445
    private function getVarName(QueryAstNode $node): string
446
    {
447
        return "var{$node->getId()}";
448
    }
449
450
    private function createReference(QueryAstNode $node): Expr
451
    {
452
        $reference = $this->php->var($this->getVarName($node));
453
        $this->setReference($node, $reference);
454
455
        return $reference;
456
    }
457
458 2
    private function setReference(QueryAstNode $node, Expr $expr): void
459
    {
460 2
        if (isset($this->references[$node->getId()])) {
461
            throw new Exception\ReferenceAlreadyExistsException($node->getId());
462
        }
463
464 2
        $this->references[$node->getId()] = $expr;
465 2
    }
466
467 3
    private function hasReference(QueryAstNode $node): bool
468
    {
469 3
        return isset($this->references[$node->getId()]);
470
    }
471
472 3
    private function getReference(QueryAstNode $node): Expr
473
    {
474 3
        if (!isset($this->references[$node->getId()])) {
475 1
            throw new Exception\ReferenceNotFoundException($node->getId());
476
        }
477
478 2
        return $this->references[$node->getId()];
479
    }
480
481
    private function addMethodCall(QueryAstNode $node, Expr $object, string $method, PhpAstNode ...$args): void
482
    {
483
        $methodCall = $this
484
            ->php
485
            ->methodCall($object, $method, $args);
486
        $this->stmts[] = new Assign($this->createReference($node), $methodCall);
487
    }
488
}
489