Completed
Push — master ( 56ea51...5cce8c )
by Edward
06:13
created

CallbackBuilder::onBeginProduction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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