Completed
Push — master ( f2f784...429167 )
by Бабичев
01:47
created

src/Flow/Flow.php (3 issues)

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 22
    public function __construct(Native $native = null, array $options = [])
124
    {
125
        // configs
126 22
        $this->mapDirectives = $options['directives'] ?? [];
127 22
        $this->folders       = $options['folders'] ?? [];
128 22
        $this->lexemes       = $options['lexemes'] ?? [];
129 22
        $this->minify        = $options['minify'] ?? false;
130 22
        $this->extends       = $options['extends'] ?? [];
131 22
        $this->debug         = $options['debug'] ?? false;
132 22
        $this->ext           = $options['ext'] ?? 'bxf';
133
134 22
        Cache::setPool($options['cache'] ?? null);
135
136
        // props
137 22
        $this->constructs = Property::get('constructs');
138
        // /props
139
140 22
        $this->pathCompile = $options['compile'] ?? sys_get_temp_dir();
141
142 22
        $this->setNative($native);
143 22
    }
144
145 11
    protected function loadLexemes(): self
146
    {
147 11
        foreach ($this->lexemes as $folder)
148
        {
149
            $this->lexeme->addFolder($folder);
150
        }
151
152 11
        return $this;
153
    }
154
155
    /**
156
     * @param Lexeme $lexeme
157
     *
158
     * @return $this
159
     */
160 11
    public function setLexeme(Lexeme $lexeme): self
161
    {
162 11
        $this->lexeme = $lexeme;
163
164 11
        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 11
    public function lexeme(): Lexeme
183
    {
184 11
        if (!$this->lexeme)
185
        {
186 11
            $this->setLexeme(new Lexeme($this));
187
        }
188
189 11
        return $this->lexeme;
190
    }
191
192
    /**
193
     * @return Lexer
194
     */
195 18
    public function lexer(): Lexer
196
    {
197 18
        if (!$this->lexer)
198
        {
199 18
            $this->lexer = new Lexer();
200
        }
201
202 18
        return $this->lexer;
203
    }
204
205
    /**
206
     * @return bool
207
     */
208 18
    public function debugMode(): bool
209
    {
210 18
        return $this->debug;
211
    }
212
213
    /**
214
     * @return FileSystem
215
     */
216 18
    public function fileSystem(): FileSystem
217
    {
218 18
        if (!$this->fileSystem)
219
        {
220 18
            $this->fileSystem = new FileSystem(
221 18
                $this,
222 18
                $this->pathCompile
223
            );
224
        }
225
226 18
        return $this->fileSystem;
227
    }
228
229
    /**
230
     * @return string
231
     */
232 18
    public function ext(): string
233
    {
234 18
        return '.' . $this->ext;
235
    }
236
237 22
    protected function setNative($native)
238
    {
239 22
        if ($native)
240
        {
241 18
            $this->native = $native;
242 18
            $this->native->setFlow($this);
243
244 18
            foreach ($this->folders as $folder => $path)
245
            {
246 18
                $this->native->addFolder($folder, $path);
247
            }
248
        }
249 22
    }
250
251
    /**
252
     * @return Native
253
     */
254 18
    public function native(): Native
255
    {
256 18
        if (!$this->native)
257
        {
258 18
            $this->setNative(new Native());
259
        }
260
261 18
        return $this->native;
262
    }
263
264
    /**
265
     * @param array $tokens
266
     *
267
     * @return string
268
     */
269
    protected function fragment(array $tokens): string
270
    {
271 11
        $data = Arr::map($tokens['tokens'] ?? $tokens, function (Token $token) {
272 11
            return $token->token;
273 11
        });
274
275 11
        return \str_replace(
276 11
            '. ',
277 11
            '.',
278 11
            \implode(' ', $data)
279
        );
280
    }
281
282 18
    public function build(array $data): string
283
    {
284 18
        $self      = $this;
285 18
        $_storeKey = __CLASS__ . JSON::encode($data);
286
287 18
        return Cache::get($_storeKey, function () use ($self, &$data) {
288 17
            return $self->buildWithoutCache($data);
289 18
        });
290
    }
291
292
    /**
293
     * @param array $data
294
     *
295
     * @return string
296
     */
297 17
    public function buildWithoutCache(array $data): string
298
    {
299 17
        $code     = [];
300 17
        $lastLast = null;
301 17
        $last     = null;
302
303
        /**
304
         * @var Token $token
305
         * @var Token $last
306
         * @var Token $lastLast
307
         */
308 17
        foreach ($data['tokens'] as $token)
309
        {
310 17
            $_token = clone $token;
311
312 17
            if ($_token->type === T_OBJECT_OPERATOR)
313
            {
314
                throw new Invalid('Undefined object operator `->`!');
315
            }
316
317 17
            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 17
            if ($last && (!$lastLast ||
333 2
                    ($lastLast->type !== T_VARIABLE &&
334 2
                        $lastLast->type !== Validator::T_ENDBRACKET &&
335 17
                        $lastLast->type !== Validator::T_ENDARRAY))
336 17
                && $last->type === Validator::T_DOT)
337
            {
338
                $pop = Arr::pop($code);
339
                Arr::push($code, '\\' . WithDirective::class . '::last()');
340
                Arr::push($code, $pop);
341
            }
342
343 17
            if ($_token->type === Validator::T_CONCAT)
344
            {
345
                $_token->token = '.';
346
            }
347
348 17
            if ((!$last || ($last && $last->type !== Validator::T_DOT)) && $_token->type === T_FUNCTION)
349
            {
350
                if (Str::ucFirst($_token->token) !== $_token->token &&
351
                    !Arr::in($this->constructs, $_token->token))
352
                {
353
                    $_token->token = '$this->helper->' . $_token->token;
354
                }
355
            }
356
357 17
            if (Arr::in([Validator::T_BRACKET, T_ARRAY], $_token->type))
358
            {
359 1
                if ($last && $last->type === Validator::T_DOT)
360
                {
361
                    Arr::pop($code);
362
                }
363
            }
364
365 17
            if (Arr::in([T_VARIABLE, T_FUNCTION], $_token->type))
366
            {
367 17
                $_token->token = \str_replace('.', '->', $_token->token);
368
369 17
                if ($last && $last->type === Validator::T_DOT)
370
                {
371
                    Arr::pop($code);
372
                    Arr::push($code, '->');
373
                }
374
375 17
                if (Str::ucFirst($_token->token) === $_token->token)
376
                {
377
                    $_token->type = T_CLASS;
378
                }
379
380 17
                if (!Arr::in([T_FUNCTION, T_CLASS], $_token->type) &&
381 17
                    (!$last || !Arr::in([
382 2
                            Validator::T_ENDBRACKET,
383 2
                            Validator::T_ENDARRAY,
384 2
                            Validator::T_DOT,
385 2
                            T_NS_SEPARATOR
386 17
                        ], $last->type)))
387
                {
388 17
                    $_token->token = '$this->' . $_token->token;
389
                }
390
            }
391
392 17
            $lastLast = $last;
393 17
            $last     = $_token;
394 17
            $code[]   = $_token->token;
395
        }
396
397 17
        return \implode($code);
0 ignored issues
show
$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

397
        return \implode(/** @scrutinizer ignore-type */ $code);
Loading history...
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

397
        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...
398
    }
399
400
    /**
401
     * @param string $view
402
     *
403
     * @return string
404
     */
405 18
    protected function minify(string $view): string
406
    {
407 18
        $html = $this->compile($view);
408
409 17
        if ($this->minify)
410
        {
411 1
            $html = \Minify_HTML::minify(\trim($html), [
412 1
                'cssMinifier' => [\Minify_CSSmin::class, 'minify'],
413
                'jsMinifier'  => [JSMin::class, 'minify'],
414
            ]);
415
        }
416
417 17
        if (!empty($this->extends))
418
        {
419 3
            $html = (new HTML($html, $this->extends))
420 3
                ->apply();
421
        }
422
423 17
        return $html;
424
    }
425
426
    /**
427
     * @param string $view
428
     *
429
     * @return string
430
     */
431 18
    public function path(string $view): string
432
    {
433 18
        if (!$this->fileSystem()->has($view))
434
        {
435 18
            $this->fileSystem()->set($view, $this->minify($view));
436
        }
437
438 17
        return $this->fileSystem()->get($view);
439
    }
440
441
    /**
442
     * @param array $rows
443
     * @param bool  $escape
444
     */
445 18
    protected function printers(array $rows, $escape = true)
446
    {
447 18
        $begin = $escape ? '\\htmlspecialchars(' : '';
448 18
        $end   = $escape ? ', ENT_QUOTES, \'UTF-8\')' : '';
449
450 18
        foreach ($rows as $row)
451
        {
452 16
            $this->tpl = $this->replace(
453 16
                $row['code'],
454 16
                '<?php echo ' . $begin . $this->build($row) . $end . '; ?>'
455
            );
456
        }
457 18
    }
458
459
    /**
460
     * @param string $key
461
     * @param array  $data
462
     * @param array  $operator
463
     *
464
     * @return mixed
465
     */
466 11
    protected function directive(string $key, array $data, array $operator)
467
    {
468 11
        $class = __NAMESPACE__ . '\\Directives\\' . Str::ucFirst($key) . 'Directive';
469
470 11
        if (isset($this->mapDirectives[$key]))
471
        {
472
            $class = $this->mapDirectives[$key];
473
        }
474
475 11
        return new $class($this, $data, $operator);
476
    }
477
478
    /**
479
     * @param string    $key
480
     * @param Directive $directive
481
     */
482 11
    protected function pushDirective(string $key, Directive $directive)
483
    {
484 11
        if (empty($this->directives[$key]))
485
        {
486 11
            $this->directives[$key] = [];
487
        }
488
489 11
        $this->directives[$key][] = $directive;
490 11
    }
491
492
    /**
493
     * @param string $key
494
     *
495
     * @return Directive
496
     */
497 6
    protected function popDirective(string $key): Directive
498
    {
499 6
        return Arr::pop($this->directives[$key]);
500
    }
501
502
    /**
503
     * @param string      $fragment
504
     * @param string      $code
505
     * @param string|null $tpl
506
     *
507
     * @return string
508
     */
509 18
    protected function replace(string $fragment, string $code, string $tpl = null): string
510
    {
511 18
        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...
512
        {
513 18
            $tpl = $this->tpl;
514
        }
515
516 18
        return \preg_replace(
517 18
            '~' . \preg_quote($fragment, '~') . '~u',
518 18
            $code,
519 18
            $tpl,
520 18
            1
521
        );
522
    }
523
524
    /**
525
     * @param array  $operator
526
     * @param string $key
527
     *
528
     * @return bool
529
     */
530 11
    protected function ifEnd($operator, string $key): bool
531
    {
532 11
        if (0 === Str::pos($key, 'end'))
533
        {
534 6
            $key  = Str::sub($key, 3);
535 6
            $data = $this->lexeme()->data($key);
536
537 6
            if (true !== $data && $this->lexeme()->closed($key))
538
            {
539 6
                $dir = $this->popDirective($key);
540
541 6
                $this->tpl = $this->replace(
542 6
                    $operator['code'],
543 6
                    $dir->endDirective()
544
                );
545
            }
546
547 6
            return !$data;
548
        }
549
550 11
        return false;
551
    }
552
553 18
    protected function operators()
554
    {
555 18
        foreach ($this->operators as $operator)
556
        {
557
            /**
558
             * @var Token $_token
559
             */
560 11
            $_token = current($operator['tokens']);
561 11
            $data   = $this->lexeme()->data($_token->token);
562
563 11
            $end = !$this->ifEnd($operator, $_token->token);
564
565 11
            if ($end && true !== $data)
566
            {
567 11
                $data = $this->lexeme()->apply(
568 11
                    $_token->token,
569 11
                    $this->fragment($operator)
570
                );
571
572
                /**
573
                 * @var Directive $directive
574
                 */
575 11
                $directive = $this->directive($_token->token, $data ?: [], $operator);
576 11
                $this->pushDirective($_token->token, $directive);
577
578 11
                $this->tpl = $this->replace(
579 11
                    $operator['code'],
580 11
                    $directive->render()
581
                );
582
            }
583
        }
584 18
    }
585
586
    /**
587
     * @param string $view
588
     * @param array  $data
589
     *
590
     * @return string
591
     */
592 18
    public function render(string $view, array $data = []): string
593
    {
594 18
        return $this->native()->render(
595 18
            $this->path($view),
596 17
            $data
597
        );
598
    }
599
600
    /**
601
     * @param string $view
602
     *
603
     * @return string
604
     */
605 18
    public function compile(string $view): string
606
    {
607 18
        $path      = $this->native()->path($view . $this->ext());
608 18
        $this->tpl = \file_get_contents($path);
609 18
        $tokens    = $this->lexer()->tokens($this->tpl);
610
611 18
        $this->literals  = $tokens[Lexer::LITERAL];
612 18
        $this->printers  = $tokens[Lexer::PRINTER];
613 18
        $this->operators = $tokens[Lexer::OPERATOR];
614 18
        $this->rows      = $tokens[Lexer::RAW];
615
616 18
        $this->printers($this->printers);
617 18
        $this->printers($this->rows, false);
618 18
        $this->operators();
619
620
        // check directives
621 18
        foreach ($this->directives as $name => $items)
622
        {
623 11
            if ($this->lexeme()->closed($name))
624
            {
625 7
                if (!empty($items))
626
                {
627 1
                    throw new Runtime(
628 1
                        \sprintf(
629 1
                            'Directive %s not closed',
630 11
                            \get_class(Arr::pop($items))
631
                        )
632
                    );
633
                }
634
            }
635
        }
636
637 17
        foreach ($this->literals as $key => $literal)
638
        {
639
            $this->tpl = \str_replace($key, $literal, $this->tpl);
640
        }
641
642 17
        return $this->tpl;
643
    }
644
645
}
646