BladeMatterParser::parseString()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Hyde\Framework\Actions;
6
7
use RuntimeException;
8
use Hyde\Facades\Filesystem;
9
10
use function str_ends_with;
11
use function str_starts_with;
12
use function substr_count;
13
use function json_decode;
14
use function explode;
15
use function strlen;
16
use function strpos;
17
use function substr;
18
use function trim;
19
20
/**
21
 * @experimental Parse the front matter in a Blade file.
22
 *
23
 * Accepts a string to make it easier to mock when testing.
24
 *
25
 * @phpstan-consistent-constructor
26
 *
27
 * === DOCUMENTATION (draft) ===
28
 *
29
 * ## Front Matter in Markdown
30
 *
31
 * HydePHP uses a special syntax called BladeMatter that allows you to define variables in a Blade file,
32
 * and have Hyde statically parse them into the front matter of the page model. This allows metadata
33
 * in your Blade pages to be used when Hyde generates dynamic data like page titles and SEO tags.
34
 *
35
 * ### Syntax
36
 *
37
 * Any line following the syntax below will be added to the parsed page object's front matter.
38
 *
39
 * @example `@php($title = 'BladeMatter Test')`
40
 * This would then be parsed into the following array in the page model: ['title' => 'BladeMatter Test']
41
 *
42
 * ### Limitations
43
 * Each directive must be on its own line, and start with `@php($.`. Arrays are currently not supported.
44
 */
45
class BladeMatterParser
46
{
47
    protected string $contents;
48
    protected array $matter;
49
50
    /** @var string The directive signature used to determine if a line should be parsed. */
51
    protected const SEARCH = '@php($';
52
53
    public static function parseFile(string $path): array
54
    {
55
        return static::parseString(Filesystem::getContents($path));
56
    }
57
58
    public static function parseString(string $contents): array
59
    {
60
        return (new static($contents))->parse()->get();
61
    }
62
63
    public function __construct(string $contents)
64
    {
65
        $this->contents = $contents;
66
    }
67
68
    public function get(): array
69
    {
70
        return $this->matter;
71
    }
72
73
    public function parse(): static
74
    {
75
        $this->matter = [];
76
77
        $lines = explode("\n", $this->contents);
78
79
        foreach ($lines as $line) {
80
            if (static::lineMatchesFrontMatter($line)) {
81
                $this->matter[static::extractKey($line)] = static::getValueWithType(static::extractValue($line));
82
            }
83
        }
84
85
        return $this;
86
    }
87
88
    protected static function lineMatchesFrontMatter(string $line): bool
89
    {
90
        return str_starts_with($line, static::SEARCH);
91
    }
92
93
    protected static function extractKey(string $line): string
94
    {
95
        // Remove search prefix
96
        $key = substr($line, strlen(static::SEARCH));
97
98
        // Remove everything after the first equals sign
99
        $key = substr($key, 0, strpos($key, '='));
100
101
        // Return trimmed line
102
        return trim($key);
103
    }
104
105
    protected static function extractValue(string $line): string
106
    {
107
        // Trim any trailing spaces and newlines
108
        $key = trim($line);
109
110
        // Remove everything before the first equals sign
111
        $key = substr($key, strpos($key, '=') + 1);
112
113
        // Remove closing parenthesis
114
        $key = substr($key, 0, strlen($key) - 1);
115
116
        // Remove any quotes so we can normalize the value
117
        $key = trim($key, ' "\'');
118
119
        // Return trimmed line
120
        return trim($key);
121
    }
122
123
    /** @return scalar|array<string, scalar> */
124
    protected static function getValueWithType(string $value): mixed
125
    {
126
        $value = trim($value);
127
128
        if ($value === 'null') {
129
            return null;
130
        }
131
132
        if (static::isValueArrayString($value)) {
133
            return static::parseArrayString($value);
134
        }
135
136
        // This will cast integers, floats, and booleans to their respective types
137
        // Still working on a way to handle multidimensional arrays and objects
138
139
        /** @var scalar|null $decoded */
140
        $decoded = json_decode($value);
141
142
        return $decoded ?? $value;
143
    }
144
145
    /** @return array<string, scalar> */
146
    protected static function parseArrayString(string $string): array
147
    {
148
        $array = [];
149
150
        // Trim input string
151
        $string = trim($string);
152
153
        // Check if string is an array
154
        if (! static::isValueArrayString($string)) {
155
            throw new RuntimeException('Failed parsing BladeMatter array. Input string must follow array syntax.');
156
        }
157
158
        // Check if string is multidimensional (not yet supported)
159
        if ((substr_count($string, '[') > 1) || (substr_count($string, ']') > 1)) {
160
            throw new RuntimeException('Failed parsing BladeMatter array. Multidimensional arrays are not supported yet.');
161
        }
162
163
        // Remove opening and closing brackets
164
        $string = substr($string, 1, strlen($string) - 2);
165
166
        // Tokenize string between commas
167
        $tokens = explode(',', $string);
168
169
        // Parse each token
170
        foreach ($tokens as $token) {
171
            // Split string into key/value pairs
172
            $pair = explode('=>', $token);
173
174
            // Add key/value pair to array
175
            $key = (string) static::getValueWithType(trim(trim($pair[0]), "'"));
176
            $value = static::getValueWithType(trim(trim($pair[1]), "'"));
177
178
            $array[$key] = $value;
179
        }
180
181
        return $array;
182
    }
183
184
    protected static function isValueArrayString(string $string): bool
185
    {
186
        return str_starts_with($string, '[') && str_ends_with($string, ']');
187
    }
188
}
189