Passed
Push — master ( 39f3e5...c21571 )
by Caen
03:18 queued 12s
created

BladeMatterParser::normalizeValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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