Passed
Push — master ( e343f8...43624f )
by Edward
05:18
created

CallbackBuilder::onFinish()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 50
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 40
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 50
ccs 0
cts 34
cp 0
crap 6
rs 9.28
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 3
            case AstNodeType::GET_INPUT:
180 2
                $this->setReference($node, $this->input);
181 2
                break;
182
183 3
            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::MERGE:
214
                /** @see ValueListFetcherInterface::merge() */
215
                $this->addMethodCall(
216
                    $node,
217
                    $this->valueListFetcher,
218
                    'merge',
219
                    ...$this->getReferences(...$node->getChildList()),
220
                );
221
                break;
222
223
            case AstNodeType::FETCH_FILTER_CONTEXT:
224
                /** @see ValueListFetcherInterface::fetchFilterContext() */
225
                $this->addMethodCall(
226
                    $node,
227
                    $this->valueListFetcher,
228
                    'fetchFilterContext',
229
                    $this->getReference($node->getChild(0)),
230
                );
231
                break;
232
233
            case AstNodeType::SPLIT_FILTER_CONTEXT:
234
                /** @see ValueListFetcherInterface::splitFilterContext() */
235
                $this->addMethodCall(
236
                    $node,
237
                    $this->valueListFetcher,
238
                    'splitFilterContext',
239
                    $this->getReference($node->getChild(0)),
240
                );
241
                break;
242
243
            case AstNodeType::JOIN_FILTER_RESULTS:
244
                /** @see ValueListFetcherInterface::joinFilterResults() */
245
                $this->addMethodCall(
246
                    $node,
247
                    $this->valueListFetcher,
248
                    'joinFilterResults',
249
                    $this->getReference($node->getChild(0)),
250
                    $this->getReference($node->getChild(1)),
251
                );
252
                break;
253
254
            case AstNodeType::FILTER:
255
                /** @see ValueListFetcherInterface::fetchFilteredValues() */
256
                $this->addMethodCall(
257
                    $node,
258
                    $this->valueListFetcher,
259
                    'fetchFilteredValues',
260
                    $this->getReference($node->getChild(0)),
261
                    $this->getReference($node->getChild(1)),
262
                );
263
                break;
264
265
            case AstNodeType::EVALUATE:
266
                /** @see EvaluatorInterface::evaluate() */
267
                $this->addMethodCall(
268
                    $node,
269
                    $this->evaluator,
270
                    'evaluate',
271
                    $this->getReference($node->getChild(0)),
272
                    $this->getReference($node->getChild(1)),
273
                );
274
                break;
275
276
            case AstNodeType::EVALUATE_LOGICAL_OR:
277
                /** @see EvaluatorInterface::logicalOr() */
278
                $this->addMethodCall(
279
                    $node,
280
                    $this->evaluator,
281
                    'logicalOr',
282
                    $this->getReference($node->getChild(0)),
283
                    $this->getReference($node->getChild(1)),
284
                );
285
                break;
286
287
            case AstNodeType::EVALUATE_LOGICAL_AND:
288
                /** @see EvaluatorInterface::logicalAnd() */
289
                $this->addMethodCall(
290
                    $node,
291
                    $this->evaluator,
292
                    'logicalAnd',
293
                    $this->getReference($node->getChild(0)),
294
                    $this->getReference($node->getChild(1)),
295
                );
296
                break;
297
298
            case AstNodeType::EVALUATE_LOGICAL_NOT:
299
                /** @see EvaluatorInterface::logicalNot() */
300
                $this->addMethodCall(
301
                    $node,
302
                    $this->evaluator,
303
                    'logicalNot',
304
                    $this->getReference($node->getChild(0)),
305
                );
306
                break;
307
308
            case AstNodeType::CALCULATE_IS_EQUAL:
309
                /** @see EvaluatorInterface::isEqual() */
310
                $this->addMethodCall(
311
                    $node,
312
                    $this->evaluator,
313
                    'isEqual',
314
                    $this->getReference($node->getChild(0)),
315
                    $this->getReference($node->getChild(1)),
316
                );
317
                break;
318
319
            case AstNodeType::CALCULATE_IS_GREATER:
320
                /** @see EvaluatorInterface::isGreater() */
321
                $this->addMethodCall(
322
                    $node,
323
                    $this->evaluator,
324
                    'isGreater',
325
                    $this->getReference($node->getChild(0)),
326
                    $this->getReference($node->getChild(1)),
327
                );
328
                break;
329
330
            case AstNodeType::CALCULATE_IS_REGEXP:
331
                /** @see EvaluatorInterface::isRegExp() */
332
                $this->addMethodCall(
333
                    $node,
334
                    $this->evaluator,
335
                    'isRegExp',
336
                    $this->php->val($node->getAttribute('pattern')),
337
                    $this->getReference($node->getChild(0)),
338
                );
339
                break;
340
341
            case AstNodeType::MATCH_ANY_CHILD:
342
                /** @see MatcherFactoryInterface::matchAnyChild() */
343
                $this->addMethodCall(
344
                    $node,
345
                    $this->matcherFactory,
346
                    'matchAnyChild',
347
                );
348
                break;
349
350
            case AstNodeType::MATCH_PROPERTY_STRICTLY:
351
                /** @see MatcherFactoryInterface::matchPropertyStrictly() */
352
                $this->addMethodCall(
353
                    $node,
354
                    $this->matcherFactory,
355
                    'matchPropertyStrictly',
356
                    ...array_map(
357
                        [$this->php, 'val'],
358
                        $node->getAttribute('names')
359
                    ),
360
                );
361
                break;
362
363
            case AstNodeType::MATCH_ELEMENT_STRICTLY:
364
                /** @see MatcherFactoryInterface::matchElementStrictly() */
365
                $this->addMethodCall(
366
                    $node,
367
                    $this->matcherFactory,
368
                    'matchElementStrictly',
369
                    ...array_map(
370
                        [$this->php, 'val'],
371
                        $node->getAttribute('indexes')
372
                    ),
373
                );
374
                break;
375
376
            case AstNodeType::MATCH_ELEMENT_SLICE:
377
                /** @see MatcherFactoryInterface::matchElementSlice() */
378
                $this->addMethodCall(
379
                    $node,
380
                    $this->matcherFactory,
381
                    'matchElementSlice',
382
                    $this
383
                        ->php
384
                        ->val($node->getAttribute('hasStart') ? $node->getAttribute('start') : null),
385
                    $this
386
                        ->php
387
                        ->val($node->getAttribute('hasEnd') ? $node->getAttribute('end') : null),
388
                    $this
389
                        ->php
390
                        ->val($node->getAttribute('step')),
391
                );
392
                break;
393
394
            case AstNodeType::AGGREGATE:
395
                /** @see EvaluatorInterface::aggregate() */
396
                $this->addMethodCall(
397
                    $node,
398
                    $this->evaluator,
399
                    'aggregate',
400
                    $this->php->val($node->getAttribute('name')),
401
                    $this->getReference($node->getChild(0)),
402
                );
403
                break;
404
405
            case AstNodeType::CREATE_LITERAL_SCALAR:
406
                $attributes = $node->getAttributeList();
407
                $value = $attributes['value'] ?? null; // TODO: allow pass null in attribute
408
                /** @see LiteralFactoryInterface::createScalar() */
409
                $this->addMethodCall(
410
                    $node,
411
                    $this->literalFactory,
412
                    'createScalar',
413
                    $this->getReference($node->getChild(0)),
414
                    $this->php->val($value),
415
                );
416
                break;
417
418
            case AstNodeType::CREATE_LITERAL_ARRAY:
419
                /** @see LiteralFactoryInterface::createArray() */
420
                $this->addMethodCall(
421
                    $node,
422
                    $this->literalFactory,
423
                    'createArray',
424
                    $this->getReference($node->getChild(0)),
425
                    new Arg(
426
                        $this->getReference($node->getChild(1)),
427
                        false,
428
                        true,
429
                    ),
430
                );
431
                break;
432
433
            case AstNodeType::CREATE_ARRAY:
434
                // [ X:APPEND_TO_ARRAY ]
435
                $items = [];
436
                foreach ($node->getChildList() as $child) {
437
                    $items[] = $this->getReference($child);
438
                }
439
                $this->stmts[] = new Assign(
440
                    $this->createReference($node),
441
                    $this->php->val($items),
442
                );
443
                break;
444
445
            case AstNodeType::APPEND_TO_ARRAY:
446
                // [ 0:<value> ]
447
                $this->setReference(
448
                    $node,
449
                    $this->getReference($node->getChild(0)),
450
                );
451
                break;
452
        }
453 2
    }
454
455
    private function getVarName(QueryAstNode $node): string
456
    {
457
        return "var{$node->getId()}";
458
    }
459
460
    private function createReference(QueryAstNode $node): Expr
461
    {
462
        $reference = $this->php->var($this->getVarName($node));
463
        $this->setReference($node, $reference);
464
465
        return $reference;
466
    }
467
468 2
    private function setReference(QueryAstNode $node, Expr $expr): void
469
    {
470 2
        if (isset($this->references[$node->getId()])) {
471
            throw new Exception\ReferenceAlreadyExistsException($node->getId());
472
        }
473
474 2
        $this->references[$node->getId()] = $expr;
475 2
    }
476
477 3
    private function hasReference(QueryAstNode $node): bool
478
    {
479 3
        return isset($this->references[$node->getId()]);
480
    }
481
482 3
    private function getReference(QueryAstNode $node): Expr
483
    {
484 3
        if (!isset($this->references[$node->getId()])) {
485 1
            throw new Exception\ReferenceNotFoundException($node->getId());
486
        }
487
488 2
        return $this->references[$node->getId()];
489
    }
490
491
    /**
492
     * @param QueryAstNode ...$nodes
493
     * @return Expr[]
494
     */
495
    private function getReferences(QueryAstNode ...$nodes): array
496
    {
497
        return array_map([$this, 'getReference'], $nodes);
498
    }
499
500
    private function addMethodCall(QueryAstNode $node, Expr $object, string $method, PhpAstNode ...$args): void
501
    {
502
        $methodCall = $this
503
            ->php
504
            ->methodCall($object, $method, $args);
505
        $this->stmts[] = new Assign($this->createReference($node), $methodCall);
506
    }
507
}
508