Completed
Push — master ( c8b954...fec39d )
by Бабичев
04:22 queued 01:46
created

Flow::compile()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 38
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.0026

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 38
ccs 20
cts 21
cp 0.9524
rs 8.439
c 1
b 0
f 0
cc 5
eloc 20
nc 7
nop 1
crap 5.0026
1
<?php
2
3
namespace Bavix\Flow;
4
5
use Bavix\Exceptions\Invalid;
6
use Bavix\Exceptions\Runtime;
7
use Bavix\Flow\Directives\WithDirective;
8
use Bavix\Flow\Minify\HTML;
9
use Bavix\Helpers\Arr;
10
use Bavix\Helpers\JSON;
11
use Bavix\Helpers\Str;
12
use Bavix\Lexer\Lexer;
13
use Bavix\Lexer\Token;
14
use Bavix\Lexer\Validator;
15
use JSMin\JSMin;
16
17
class Flow
18
{
19
20
    const VERSION = '1.0.4';
21
22
    /**
23
     * @var string
24
     */
25
    protected $ext;
26
27
    /**
28
     * @var Lexer
29
     */
30
    protected $lexer;
31
32
    /**
33
     * @var Lexeme
34
     */
35
    protected $lexeme;
36
37
    /**
38
     * @var array
39
     */
40
    protected $literals;
41
42
    /**
43
     * @var array
44
     */
45
    protected $printers;
46
47
    /**
48
     * @var array
49
     */
50
    protected $operators;
51
52
    /**
53
     * @var array
54
     */
55
    protected $directives = [];
56
57
    /**
58
     * @var array
59
     */
60
    protected $mapDirectives = [];
61
62
    /**
63
     * @var array
64
     */
65
    protected $constructs = [];
66
67
    /**
68
     * @var array
69
     */
70
    protected $lexemes = [];
71
72
    /**
73
     * @var array
74
     */
75
    protected $folders = [];
76
77
    /**
78
     * @var array
79
     */
80
    protected $rows;
81
82
    /**
83
     * @var Native
84
     */
85
    protected $native;
86
87
    /**
88
     * @var FileSystem
89
     */
90
    protected $fileSystem;
91
92
    /**
93
     * @var string
94
     */
95
    protected $tpl;
96
97
    /**
98
     * @var bool
99
     */
100
    protected $debug;
101
102
    /**
103
     * @var bool
104
     */
105
    protected $minify;
106
107
    /**
108
     * @var array
109
     */
110
    protected $extends;
111
112
    /**
113
     * @var string
114
     */
115
    protected $pathCompile;
116
117
    /**
118
     * Flow constructor.
119
     *
120
     * @param Native $native
121
     * @param array  $options
122
     */
123 24
    public function __construct(Native $native = null, array $options = [])
124
    {
125
        // configs
126 24
        $this->mapDirectives = $options['directives'] ?? [];
127 24
        $this->folders       = $options['folders'] ?? [];
128 24
        $this->lexemes       = $options['lexemes'] ?? [];
129 24
        $this->minify        = $options['minify'] ?? false;
130 24
        $this->extends       = $options['extends'] ?? [];
131 24
        $this->debug         = $options['debug'] ?? false;
132 24
        $this->ext           = $options['ext'] ?? 'bxf';
133
134 24
        Cache::setPool($options['cache'] ?? null);
135
136
        // props
137 24
        $this->constructs = Property::get('constructs');
138
        // /props
139
140 24
        $this->pathCompile = $options['compile'] ?? sys_get_temp_dir();
141
142 24
        $this->setNative($native);
143 24
    }
144
145 13
    protected function loadLexemes(): self
146
    {
147 13
        foreach ($this->lexemes as $folder)
148
        {
149
            $this->lexeme->addFolder($folder);
150
        }
151
152 13
        return $this;
153
    }
154
155
    /**
156
     * @param Lexeme $lexeme
157
     *
158
     * @return $this
159
     */
160 13
    public function setLexeme(Lexeme $lexeme): self
161
    {
162 13
        $this->lexeme = $lexeme;
163
164 13
        return $this->loadLexemes();
165
    }
166
167
    /**
168
     * @param Lexer $lexer
169
     *
170
     * @return $this
171
     */
172
    public function setLexer(Lexer $lexer): self
173
    {
174
        $this->lexer = $lexer;
175
176
        return $this;
177
    }
178
179
    /**
180
     * @return Lexeme
181
     */
182 13
    public function lexeme(): Lexeme
183
    {
184 13
        if (!$this->lexeme)
185
        {
186 13
            $this->setLexeme(new Lexeme($this));
187
        }
188
189 13
        return $this->lexeme;
190
    }
191
192
    /**
193
     * @return Lexer
194
     */
195 20
    public function lexer(): Lexer
196
    {
197 20
        if (!$this->lexer)
198
        {
199 20
            $this->lexer = new Lexer();
200
        }
201
202 20
        return $this->lexer;
203
    }
204
205
    /**
206
     * @return bool
207
     */
208 20
    public function debugMode(): bool
209
    {
210 20
        return $this->debug;
211
    }
212
213
    /**
214
     * @return FileSystem
215
     */
216 20
    public function fileSystem(): FileSystem
217
    {
218 20
        if (!$this->fileSystem)
219
        {
220 20
            $this->fileSystem = new FileSystem(
221 20
                $this,
222 20
                $this->pathCompile
223
            );
224
        }
225
226 20
        return $this->fileSystem;
227
    }
228
229
    /**
230
     * @return string
231
     */
232 20
    public function ext(): string
233
    {
234 20
        return '.' . $this->ext;
235
    }
236
237 24
    protected function setNative($native)
238
    {
239 24
        if ($native)
240
        {
241 20
            $this->native = $native;
242 20
            $this->native->setFlow($this);
243
244 20
            foreach ($this->folders as $folder => $path)
245
            {
246 20
                $this->native->addFolder($folder, $path);
247
            }
248
        }
249 24
    }
250
251
    /**
252
     * @return Native
253
     */
254 20
    public function native(): Native
255
    {
256 20
        if (!$this->native)
257
        {
258 20
            $this->setNative(new Native());
259
        }
260
261 20
        return $this->native;
262
    }
263
264
    /**
265
     * @param array $tokens
266
     *
267
     * @return string
268
     */
269
    protected function fragment(array $tokens): string
270
    {
271 13
        $data = Arr::map($tokens['tokens'] ?? $tokens, function (Token $token) {
272 13
            return $token->token;
273 13
        });
274
275 13
        return \str_replace(
276 13
            '. ',
277 13
            '.',
278 13
            \implode(' ', $data)
279
        );
280
    }
281
282 20
    public function build(array $data): string
283
    {
284 20
        $self      = $this;
285 20
        $_storeKey = __CLASS__ . JSON::encode($data);
286
287 20
        return Cache::get($_storeKey, function () use ($self, &$data) {
288 19
            return $self->buildWithoutCache($data);
289 20
        });
290
    }
291
292
    /**
293
     * @param array $data
294
     *
295
     * @return string
296
     */
297 19
    public function buildWithoutCache(array $data): string
298
    {
299 19
        $code     = [];
300 19
        $lastLast = null;
301 19
        $last     = null;
302
303
        /**
304
         * @var Token $token
305
         * @var Token $last
306
         * @var Token $lastLast
307
         */
308 19
        foreach ($data['tokens'] as $token)
309
        {
310 19
            $_token = clone $token;
311
312 19
            if ($_token->type === T_OBJECT_OPERATOR)
313
            {
314
                throw new Invalid('Undefined object operator `->`!');
315
            }
316
317 19
            if (Arr::in([T_NEW, T_CLONE, T_INSTEADOF, T_INSTANCEOF, T_AS], $_token->type))
318
            {
319
                $lastLast = $last;
320
                $last     = $_token;
321
322
                if (!Arr::in([T_NEW, T_CLASS], $_token->type))
323
                {
324
                    $code[] = ' ';
325
                }
326
327
                $code[] = $_token->token;
328
                $code[] = ' ';
329
                continue;
330
            }
331
332 19
            if ($last && (!$lastLast ||
333 4
                    ($lastLast->type !== T_VARIABLE &&
334 4
                        $lastLast->type !== Validator::T_ENDBRACKET &&
335 19
                        $lastLast->type !== Validator::T_ENDARRAY))
336 19
                && $last->type === Validator::T_DOT)
337
            {
338 1
                $pop = Arr::pop($code);
339 1
                Arr::push($code, '\\' . WithDirective::class . '::last()');
340 1
                Arr::push($code, $pop);
341
            }
342
343 19
            if ($_token->type === Validator::T_CONCAT)
344
            {
345
                $_token->token = '.';
346
            }
347
348 19
            if ((!$last ||
349 19
                ($last && !Arr::in([\T_DOUBLE_COLON, Validator::T_DOT], $last->type))) &&
350 19
                $_token->type === T_FUNCTION)
351
            {
352
                if (Str::ucFirst($_token->token) !== $_token->token &&
353
                    !Arr::in($this->constructs, $_token->token))
354
                {
355
                    $_token->token = '$this->helper->' . $_token->token;
356
                }
357
            }
358
359 19
            if (Arr::in([Validator::T_BRACKET, T_ARRAY], $_token->type))
360
            {
361 3
                if ($last && $last->type === Validator::T_DOT)
362
                {
363 1
                    Arr::pop($code);
364
                }
365
            }
366
367 19
            if (Arr::in([T_VARIABLE, T_FUNCTION], $_token->type))
368
            {
369 19
                $_token->token = \str_replace('.', '->', $_token->token);
370
371 19
                if ($last && $last->type === Validator::T_DOT)
372
                {
373
                    Arr::pop($code);
374
                    Arr::push($code, '->');
375
                }
376
377 19
                if (Str::ucFirst($_token->token) === $_token->token)
378
                {
379 1
                    $_token->type = T_CLASS;
380
                }
381
382 19
                if (!Arr::in([T_FUNCTION, T_CLASS], $_token->type) &&
383 19
                    (!$last || !Arr::in([
384 3
                            Validator::T_ENDBRACKET,
385 3
                            Validator::T_ENDARRAY,
386 3
                            Validator::T_DOT,
387 3
                            T_NS_SEPARATOR,
388
                            \T_DOUBLE_COLON
389 19
                        ], $last->type)))
390
                {
391 19
                    $_token->token = '$this->' . $_token->token;
392
                }
393
            }
394
395 19
            $lastLast = $last;
396 19
            $last     = $_token;
397 19
            $code[]   = $_token->token;
398
        }
399
400 19
        return \implode($code);
0 ignored issues
show
Bug introduced by
The call to implode() has too few arguments starting with pieces. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

400
        return /** @scrutinizer ignore-call */ \implode($code);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
$code of type array|string[] is incompatible with the type string expected by parameter $glue of implode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

400
        return \implode(/** @scrutinizer ignore-type */ $code);
Loading history...
401
    }
402
403
    /**
404
     * @param string $view
405
     *
406
     * @return string
407
     */
408 20
    protected function minify(string $view): string
409
    {
410 20
        $html = $this->compile($view);
411
412 19
        if ($this->minify)
413
        {
414 1
            $html = \Minify_HTML::minify(\trim($html), [
415 1
                'cssMinifier' => [\Minify_CSSmin::class, 'minify'],
416
                'jsMinifier'  => [JSMin::class, 'minify'],
417
            ]);
418
        }
419
420 19
        if (!empty($this->extends))
421
        {
422 3
            $html = (new HTML($html, $this->extends))
423 3
                ->apply();
424
        }
425
426 19
        return $html;
427
    }
428
429
    /**
430
     * @param string $view
431
     *
432
     * @return string
433
     */
434 20
    public function path(string $view): string
435
    {
436 20
        if (!$this->fileSystem()->has($view))
437
        {
438 20
            $this->fileSystem()->set($view, $this->minify($view));
439
        }
440
441 19
        return $this->fileSystem()->get($view);
442
    }
443
444
    /**
445
     * @param array $rows
446
     * @param bool  $escape
447
     */
448 20
    protected function printers(array $rows, $escape = true)
449
    {
450 20
        $begin = $escape ? '\\htmlspecialchars(' : '';
451 20
        $end   = $escape ? ', ENT_QUOTES, \'UTF-8\')' : '';
452
453 20
        foreach ($rows as $row)
454
        {
455 18
            $this->tpl = $this->replace(
456 18
                $row['code'],
457 18
                '<?php echo ' . $begin . $this->build($row) . $end . '; ?>'
458
            );
459
        }
460 20
    }
461
462
    /**
463
     * @param string $key
464
     * @param array  $data
465
     * @param array  $operator
466
     *
467
     * @return mixed
468
     */
469 13
    protected function directive(string $key, array $data, array $operator)
470
    {
471 13
        $class = __NAMESPACE__ . '\\Directives\\' . Str::ucFirst($key) . 'Directive';
472
473 13
        if (isset($this->mapDirectives[$key]))
474
        {
475
            $class = $this->mapDirectives[$key];
476
        }
477
478 13
        return new $class($this, $data, $operator);
479
    }
480
481
    /**
482
     * @param string    $key
483
     * @param Directive $directive
484
     */
485 13
    protected function pushDirective(string $key, Directive $directive)
486
    {
487 13
        if (empty($this->directives[$key]))
488
        {
489 13
            $this->directives[$key] = [];
490
        }
491
492 13
        $this->directives[$key][] = $directive;
493 13
    }
494
495
    /**
496
     * @param string $key
497
     *
498
     * @return Directive
499
     */
500 7
    protected function popDirective(string $key): Directive
501
    {
502 7
        return Arr::pop($this->directives[$key]);
503
    }
504
505
    /**
506
     * @param string      $fragment
507
     * @param string      $code
508
     * @param string|null $tpl
509
     *
510
     * @return string
511
     */
512 20
    protected function replace(string $fragment, string $code, string $tpl = null): string
513
    {
514 20
        if (!$tpl)
0 ignored issues
show
Bug Best Practice introduced by
The expression $tpl of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
515
        {
516 20
            $tpl = $this->tpl;
517
        }
518
519 20
        return \preg_replace(
520 20
            '~' . \preg_quote($fragment, '~') . '~u',
521 20
            $code,
522 20
            $tpl,
523 20
            1
524
        );
525
    }
526
527
    /**
528
     * @param array  $operator
529
     * @param string $key
530
     *
531
     * @return bool
532
     */
533 13
    protected function ifEnd($operator, string $key): bool
534
    {
535 13
        if (0 === Str::pos($key, 'end'))
536
        {
537 7
            $key  = Str::sub($key, 3);
538 7
            $data = $this->lexeme()->data($key);
539
540 7
            if (true !== $data && $this->lexeme()->closed($key))
541
            {
542 7
                $dir = $this->popDirective($key);
543
544 7
                $this->tpl = $this->replace(
545 7
                    $operator['code'],
546 7
                    $dir->endDirective()
547
                );
548
            }
549
550 7
            return !$data;
551
        }
552
553 13
        return false;
554
    }
555
556 20
    protected function operators()
557
    {
558 20
        foreach ($this->operators as $operator)
559
        {
560
            /**
561
             * @var Token $_token
562
             */
563 13
            $_token = current($operator['tokens']);
564 13
            $data   = $this->lexeme()->data($_token->token);
565
566 13
            $end = !$this->ifEnd($operator, $_token->token);
567
568 13
            if ($end && true !== $data)
569
            {
570 13
                $data = $this->lexeme()->apply(
571 13
                    $_token->token,
572 13
                    $this->fragment($operator)
573
                );
574
575
                /**
576
                 * @var Directive $directive
577
                 */
578 13
                $directive = $this->directive($_token->token, $data ?: [], $operator);
579 13
                $this->pushDirective($_token->token, $directive);
580
581 13
                $this->tpl = $this->replace(
582 13
                    $operator['code'],
583 13
                    $directive->render()
584
                );
585
            }
586
        }
587 20
    }
588
589
    /**
590
     * @param string $view
591
     * @param array  $data
592
     *
593
     * @return string
594
     */
595 20
    public function render(string $view, array $data = []): string
596
    {
597 20
        return $this->native()->render(
598 20
            $this->path($view),
599 19
            $data
600
        );
601
    }
602
603
    /**
604
     * @param string $view
605
     *
606
     * @return string
607
     */
608 20
    public function compile(string $view): string
609
    {
610 20
        $path      = $this->native()->path($view . $this->ext());
611 20
        $this->tpl = \file_get_contents($path);
612 20
        $tokens    = $this->lexer()->tokens($this->tpl);
613
614 20
        $this->literals  = $tokens[Lexer::LITERAL];
615 20
        $this->printers  = $tokens[Lexer::PRINTER];
616 20
        $this->operators = $tokens[Lexer::OPERATOR];
617 20
        $this->rows      = $tokens[Lexer::RAW];
618
619 20
        $this->printers($this->printers);
620 20
        $this->printers($this->rows, false);
621 20
        $this->operators();
622
623
        // check directives
624 20
        foreach ($this->directives as $name => $items)
625
        {
626 13
            if ($this->lexeme()->closed($name))
627
            {
628 8
                if (!empty($items))
629
                {
630 1
                    throw new Runtime(
631 1
                        \sprintf(
632 1
                            'Directive %s not closed',
633 13
                            \get_class(Arr::pop($items))
634
                        )
635
                    );
636
                }
637
            }
638
        }
639
640 19
        foreach ($this->literals as $key => $literal)
641
        {
642
            $this->tpl = \str_replace($key, $literal, $this->tpl);
643
        }
644
645 19
        return $this->tpl;
646
    }
647
648
}
649