+165
src/Generator/StubRenderer.php
+165
src/Generator/StubRenderer.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Generator;
4
+
5
+
use SocialDept\Schema\Exceptions\GenerationException;
6
+
7
+
class StubRenderer
8
+
{
9
+
/**
10
+
* Path to stub files.
11
+
*/
12
+
protected string $stubPath;
13
+
14
+
/**
15
+
* Cached stub contents.
16
+
*
17
+
* @var array<string, string>
18
+
*/
19
+
protected array $stubs = [];
20
+
21
+
/**
22
+
* Create a new StubRenderer.
23
+
*/
24
+
public function __construct(?string $stubPath = null)
25
+
{
26
+
$this->stubPath = $stubPath ?? $this->getDefaultStubPath();
27
+
}
28
+
29
+
/**
30
+
* Render a stub with variables.
31
+
*
32
+
* @param array<string, mixed> $variables
33
+
*/
34
+
public function render(string $stub, array $variables): string
35
+
{
36
+
$template = $this->loadStub($stub);
37
+
38
+
return $this->replaceVariables($template, $variables);
39
+
}
40
+
41
+
/**
42
+
* Load a stub file.
43
+
*/
44
+
protected function loadStub(string $name): string
45
+
{
46
+
if (isset($this->stubs[$name])) {
47
+
return $this->stubs[$name];
48
+
}
49
+
50
+
$path = $this->getStubPath($name);
51
+
52
+
if (! file_exists($path)) {
53
+
throw GenerationException::templateNotFound($name);
54
+
}
55
+
56
+
$content = file_get_contents($path);
57
+
58
+
if ($content === false) {
59
+
throw GenerationException::cannotReadFile($path);
60
+
}
61
+
62
+
$this->stubs[$name] = $content;
63
+
64
+
return $content;
65
+
}
66
+
67
+
/**
68
+
* Replace variables in template.
69
+
*
70
+
* @param array<string, mixed> $variables
71
+
*/
72
+
protected function replaceVariables(string $template, array $variables): string
73
+
{
74
+
$result = $template;
75
+
76
+
foreach ($variables as $key => $value) {
77
+
// Convert value to string
78
+
$stringValue = $this->valueToString($value);
79
+
80
+
// Replace {{ key }} with value
81
+
$result = str_replace('{{ '.$key.' }}', $stringValue, $result);
82
+
}
83
+
84
+
// Remove any remaining unreplaced variables
85
+
$result = preg_replace('/\{\{\s*\w+\s*\}\}/', '', $result);
86
+
87
+
return $result;
88
+
}
89
+
90
+
/**
91
+
* Convert a value to string for replacement.
92
+
*/
93
+
protected function valueToString(mixed $value): string
94
+
{
95
+
if (is_array($value)) {
96
+
return implode("\n", array_filter($value));
97
+
}
98
+
99
+
if (is_bool($value)) {
100
+
return $value ? 'true' : 'false';
101
+
}
102
+
103
+
if ($value === null) {
104
+
return '';
105
+
}
106
+
107
+
return (string) $value;
108
+
}
109
+
110
+
/**
111
+
* Get the path for a stub file.
112
+
*/
113
+
protected function getStubPath(string $name): string
114
+
{
115
+
// Check for published stubs first (in Laravel app)
116
+
$publishedPath = base_path('stubs/schema/'.$name.'.stub');
117
+
if (file_exists($publishedPath)) {
118
+
return $publishedPath;
119
+
}
120
+
121
+
// Fall back to package stubs
122
+
return $this->stubPath.'/'.$name.'.stub';
123
+
}
124
+
125
+
/**
126
+
* Get default stub path.
127
+
*/
128
+
protected function getDefaultStubPath(): string
129
+
{
130
+
return __DIR__.'/../../stubs';
131
+
}
132
+
133
+
/**
134
+
* Clear cached stubs.
135
+
*/
136
+
public function clearCache(): void
137
+
{
138
+
$this->stubs = [];
139
+
}
140
+
141
+
/**
142
+
* Set custom stub path.
143
+
*/
144
+
public function setStubPath(string $path): void
145
+
{
146
+
$this->stubPath = $path;
147
+
$this->clearCache();
148
+
}
149
+
150
+
/**
151
+
* Get available stubs.
152
+
*
153
+
* @return array<string>
154
+
*/
155
+
public function getAvailableStubs(): array
156
+
{
157
+
$stubs = [];
158
+
159
+
foreach (glob($this->stubPath.'/*.stub') as $file) {
160
+
$stubs[] = basename($file, '.stub');
161
+
}
162
+
163
+
return $stubs;
164
+
}
165
+
}
+15
stubs/class.stub
+15
stubs/class.stub
+6
stubs/constructor.stub
+6
stubs/constructor.stub
+5
stubs/method.stub
+5
stubs/method.stub
+2
stubs/property.stub
+2
stubs/property.stub
+218
tests/Unit/Generator/StubRendererTest.php
+218
tests/Unit/Generator/StubRendererTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Generator;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Exceptions\GenerationException;
7
+
use SocialDept\Schema\Generator\StubRenderer;
8
+
9
+
class StubRendererTest extends TestCase
10
+
{
11
+
protected StubRenderer $renderer;
12
+
13
+
protected string $stubPath;
14
+
15
+
protected function setUp(): void
16
+
{
17
+
parent::setUp();
18
+
19
+
$this->stubPath = __DIR__.'/../../fixtures/stubs';
20
+
@mkdir($this->stubPath, 0755, true);
21
+
22
+
$this->renderer = new StubRenderer($this->stubPath);
23
+
}
24
+
25
+
protected function tearDown(): void
26
+
{
27
+
// Clean up test stubs
28
+
if (is_dir($this->stubPath)) {
29
+
foreach (glob($this->stubPath.'/*.stub') as $file) {
30
+
@unlink($file);
31
+
}
32
+
@rmdir($this->stubPath);
33
+
}
34
+
35
+
parent::tearDown();
36
+
}
37
+
38
+
public function test_it_renders_simple_stub(): void
39
+
{
40
+
file_put_contents($this->stubPath.'/simple.stub', 'Hello {{ name }}!');
41
+
42
+
$result = $this->renderer->render('simple', ['name' => 'World']);
43
+
44
+
$this->assertSame('Hello World!', $result);
45
+
}
46
+
47
+
public function test_it_renders_multiple_variables(): void
48
+
{
49
+
file_put_contents(
50
+
$this->stubPath.'/multiple.stub',
51
+
'namespace {{ namespace }};
52
+
53
+
class {{ className }}
54
+
{
55
+
}'
56
+
);
57
+
58
+
$result = $this->renderer->render('multiple', [
59
+
'namespace' => 'App\\Models',
60
+
'className' => 'Post',
61
+
]);
62
+
63
+
$this->assertStringContainsString('namespace App\\Models;', $result);
64
+
$this->assertStringContainsString('class Post', $result);
65
+
}
66
+
67
+
public function test_it_handles_array_values(): void
68
+
{
69
+
file_put_contents(
70
+
$this->stubPath.'/array.stub',
71
+
'class Test
72
+
{
73
+
{{ properties }}
74
+
}'
75
+
);
76
+
77
+
$result = $this->renderer->render('array', [
78
+
'properties' => [
79
+
' public string $name;',
80
+
' public int $age;',
81
+
],
82
+
]);
83
+
84
+
$this->assertStringContainsString('public string $name;', $result);
85
+
$this->assertStringContainsString('public int $age;', $result);
86
+
}
87
+
88
+
public function test_it_handles_empty_values(): void
89
+
{
90
+
file_put_contents($this->stubPath.'/empty.stub', 'Hello{{ suffix }}');
91
+
92
+
$result = $this->renderer->render('empty', ['suffix' => '']);
93
+
94
+
$this->assertSame('Hello', $result);
95
+
}
96
+
97
+
public function test_it_handles_null_values(): void
98
+
{
99
+
file_put_contents($this->stubPath.'/null.stub', 'Hello{{ suffix }}');
100
+
101
+
$result = $this->renderer->render('null', ['suffix' => null]);
102
+
103
+
$this->assertSame('Hello', $result);
104
+
}
105
+
106
+
public function test_it_removes_unreplaced_variables(): void
107
+
{
108
+
file_put_contents($this->stubPath.'/unreplaced.stub', 'Hello {{ name }}{{ extra }}');
109
+
110
+
$result = $this->renderer->render('unreplaced', ['name' => 'World']);
111
+
112
+
$this->assertSame('Hello World', $result);
113
+
$this->assertStringNotContainsString('{{ extra }}', $result);
114
+
}
115
+
116
+
public function test_it_caches_stub_contents(): void
117
+
{
118
+
file_put_contents($this->stubPath.'/cached.stub', 'Original');
119
+
120
+
// First render
121
+
$result1 = $this->renderer->render('cached', []);
122
+
123
+
// Change file content
124
+
file_put_contents($this->stubPath.'/cached.stub', 'Modified');
125
+
126
+
// Second render should still use cached version
127
+
$result2 = $this->renderer->render('cached', []);
128
+
129
+
$this->assertSame('Original', $result1);
130
+
$this->assertSame('Original', $result2);
131
+
}
132
+
133
+
public function test_it_can_clear_cache(): void
134
+
{
135
+
file_put_contents($this->stubPath.'/clearable.stub', 'Original');
136
+
137
+
// First render
138
+
$result1 = $this->renderer->render('clearable', []);
139
+
140
+
// Change file and clear cache
141
+
file_put_contents($this->stubPath.'/clearable.stub', 'Modified');
142
+
$this->renderer->clearCache();
143
+
144
+
// Should load new version
145
+
$result2 = $this->renderer->render('clearable', []);
146
+
147
+
$this->assertSame('Original', $result1);
148
+
$this->assertSame('Modified', $result2);
149
+
}
150
+
151
+
public function test_it_throws_when_stub_not_found(): void
152
+
{
153
+
$this->expectException(GenerationException::class);
154
+
$this->expectExceptionMessage('Template not found: nonexistent');
155
+
156
+
$this->renderer->render('nonexistent', []);
157
+
}
158
+
159
+
public function test_it_can_set_custom_stub_path(): void
160
+
{
161
+
$customPath = __DIR__.'/../../fixtures/custom-stubs';
162
+
@mkdir($customPath, 0755, true);
163
+
file_put_contents($customPath.'/custom.stub', 'Custom stub');
164
+
165
+
$this->renderer->setStubPath($customPath);
166
+
$result = $this->renderer->render('custom', []);
167
+
168
+
$this->assertSame('Custom stub', $result);
169
+
170
+
// Cleanup
171
+
@unlink($customPath.'/custom.stub');
172
+
@rmdir($customPath);
173
+
}
174
+
175
+
public function test_it_prefers_published_stubs(): void
176
+
{
177
+
// This test would require a full Laravel app context
178
+
// For now, we just test that the method checks for published stubs
179
+
file_put_contents($this->stubPath.'/package.stub', 'Package version');
180
+
181
+
$result = $this->renderer->render('package', []);
182
+
183
+
$this->assertSame('Package version', $result);
184
+
}
185
+
186
+
public function test_it_lists_available_stubs(): void
187
+
{
188
+
file_put_contents($this->stubPath.'/stub1.stub', 'Stub 1');
189
+
file_put_contents($this->stubPath.'/stub2.stub', 'Stub 2');
190
+
file_put_contents($this->stubPath.'/stub3.stub', 'Stub 3');
191
+
192
+
$stubs = $this->renderer->getAvailableStubs();
193
+
194
+
$this->assertContains('stub1', $stubs);
195
+
$this->assertContains('stub2', $stubs);
196
+
$this->assertContains('stub3', $stubs);
197
+
}
198
+
199
+
public function test_it_handles_boolean_values(): void
200
+
{
201
+
file_put_contents($this->stubPath.'/boolean.stub', 'Active: {{ active }}');
202
+
203
+
$result1 = $this->renderer->render('boolean', ['active' => true]);
204
+
$result2 = $this->renderer->render('boolean', ['active' => false]);
205
+
206
+
$this->assertSame('Active: true', $result1);
207
+
$this->assertSame('Active: false', $result2);
208
+
}
209
+
210
+
public function test_it_handles_numeric_values(): void
211
+
{
212
+
file_put_contents($this->stubPath.'/numeric.stub', 'Count: {{ count }}');
213
+
214
+
$result = $this->renderer->render('numeric', ['count' => 42]);
215
+
216
+
$this->assertSame('Count: 42', $result);
217
+
}
218
+
}