1use super::scaffolder::{ProjectScaffolder, ScaffoldedFile};
7use anyhow::Result;
8use std::path::{Path, PathBuf};
9
10pub struct PhpScaffolder;
12
13impl ProjectScaffolder for PhpScaffolder {
14 #[allow(clippy::vec_init_then_push)]
15 fn scaffold(&self, _project_dir: &Path, project_name: &str) -> Result<Vec<ScaffoldedFile>> {
16 let mut files = Vec::new();
17
18 files.push(ScaffoldedFile::new(
20 PathBuf::from("composer.json"),
21 self.generate_composer_json(project_name),
22 ));
23
24 files.push(ScaffoldedFile::new(
26 PathBuf::from("phpstan.neon"),
27 self.generate_phpstan_neon(),
28 ));
29
30 files.push(ScaffoldedFile::new(
32 PathBuf::from("phpunit.xml"),
33 self.generate_phpunit_xml(),
34 ));
35
36 files.push(ScaffoldedFile::new(
38 PathBuf::from("src/AppController.php"),
39 self.generate_app_php(),
40 ));
41
42 files.push(ScaffoldedFile::new(
44 PathBuf::from("bin/server.php"),
45 self.generate_server_php(),
46 ));
47
48 files.push(ScaffoldedFile::new(
50 PathBuf::from("tests/AppTest.php"),
51 self.generate_app_test_php(),
52 ));
53
54 files.push(ScaffoldedFile::new(
56 PathBuf::from(".gitignore"),
57 self.generate_gitignore(),
58 ));
59
60 files.push(ScaffoldedFile::new(
62 PathBuf::from("README.md"),
63 self.generate_readme(project_name),
64 ));
65
66 Ok(files)
67 }
68
69 fn next_steps(&self, project_name: &str) -> Vec<String> {
70 vec![
71 format!("cd {}", project_name),
72 "composer install".to_string(),
73 "php bin/server.php".to_string(),
74 ]
75 }
76}
77
78impl PhpScaffolder {
79 fn generate_composer_json(&self, project_name: &str) -> String {
80 let version = env!("CARGO_PKG_VERSION");
81 let package_name = project_name.replace('_', "-").to_lowercase();
82 format!(
83 r#"{{
84 "name": "your-vendor/{package_name}",
85 "description": "Spikard PHP application",
86 "type": "project",
87 "require": {{
88 "php": "^8.2",
89 "spikard/spikard": "^{version}"
90 }},
91 "require-dev": {{
92 "phpunit/phpunit": "^11.0",
93 "phpstan/phpstan": "^1.10"
94 }},
95 "autoload": {{
96 "psr-4": {{
97 "App\\": "src/"
98 }}
99 }},
100 "autoload-dev": {{
101 "psr-4": {{
102 "App\\Tests\\": "tests/"
103 }}
104 }},
105 "authors": [
106 {{
107 "name": "Your Name",
108 "email": "you@example.com"
109 }}
110 ],
111 "license": "MIT",
112 "scripts": {{
113 "serve": "php bin/server.php",
114 "test": "vendor/bin/phpunit --configuration phpunit.xml",
115 "phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon"
116 }}
117}}
118"#
119 )
120 }
121
122 fn generate_phpstan_neon(&self) -> String {
123 r"parameters:
124 level: max
125 paths:
126 - src
127 - tests
128 excludePaths:
129 - */vendor/*
130 treatPhpDocTypesAsCertain: false
131 checkMissingIterableValueType: false
132"
133 .to_string()
134 }
135
136 fn generate_phpunit_xml(&self) -> String {
137 r#"<?xml version="1.0" encoding="UTF-8"?>
138<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
139 xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
140 bootstrap="vendor/autoload.php"
141 cacheDirectory=".phpunit.cache"
142 colors="true"
143 verbose="true">
144 <testsuites>
145 <testsuite name="Unit Tests">
146 <directory>tests</directory>
147 </testsuite>
148 </testsuites>
149
150 <coverage processUncoveredFiles="true">
151 <include>
152 <directory suffix=".php">src</directory>
153 </include>
154 <exclude>
155 <directory>tests</directory>
156 </exclude>
157 </coverage>
158</phpunit>
159"#
160 .to_string()
161 }
162
163 fn generate_app_php(&self) -> String {
164 r"<?php
165
166declare(strict_types=1);
167
168namespace App;
169
170use Spikard\Attributes\Get;
171use Spikard\Http\Response;
172
173/**
174 * Main application controller
175 *
176 * Demonstrates a simple Spikard application with a health check endpoint.
177 */
178final class AppController
179{
180 #[Get('/health')]
181 public function health(): Response
182 {
183 return Response::json(['status' => 'healthy', 'message' => 'Server is running']);
184 }
185
186 #[Get('/')]
187 public function index(): Response
188 {
189 return Response::text('Welcome to Spikard PHP');
190 }
191}
192"
193 .to_string()
194 }
195
196 fn generate_server_php(&self) -> String {
197 r#"<?php
198
199declare(strict_types=1);
200
201require_once __DIR__ . '/../vendor/autoload.php';
202
203use App\AppController;
204use Spikard\App;
205use Spikard\Config\ServerConfig;
206
207$config = new ServerConfig(port: 8000);
208$app = (new App($config))->registerController(new AppController());
209
210echo "Starting server on http://127.0.0.1:8000\n";
211echo "Press Ctrl+C to stop\n\n";
212
213$app->run();
214"#
215 .to_string()
216 }
217
218 fn generate_app_test_php(&self) -> String {
219 r"<?php
220
221declare(strict_types=1);
222
223namespace App\Tests;
224
225use App\AppController;
226use PHPUnit\Framework\TestCase;
227use Spikard\Http\Response;
228
229/**
230 * Tests for the main application
231 */
232final class AppTest extends TestCase
233{
234 public function testControllerCreatesResponses(): void
235 {
236 $controller = new AppController();
237
238 $this->assertInstanceOf(Response::class, $controller->health());
239 $this->assertInstanceOf(Response::class, $controller->index());
240 }
241}
242"
243 .to_string()
244 }
245
246 fn generate_gitignore(&self) -> String {
247 r"# Dependencies
248/vendor/
249
250# IDE
251.vscode/
252.idea/
253*.swp
254*.swo
255*~
256
257# PHP
258.php-version
259
260# Testing
261.phpunit.cache/
262coverage/
263
264# Environment
265.env
266.env.local
267.env.*.local
268
269# OS
270.DS_Store
271Thumbs.db
272"
273 .to_string()
274 }
275
276 fn generate_readme(&self, project_name: &str) -> String {
277 format!(
278 r"# {project_name}
279
280A Spikard PHP application.
281
282## Requirements
283
284- PHP 8.2+
285- Composer
286
287## Installation
288
289```bash
290composer install
291```
292
293## Running the Application
294
295```bash
296php bin/server.php
297```
298
299The server will start on `http://127.0.0.1:8000`.
300
301## Testing
302
303```bash
304composer test
305```
306
307## Static Analysis
308
309```bash
310composer phpstan
311```
312
313## Next Steps
314
3151. Install dependencies: `composer install`
3162. Run the server: `php bin/server.php`
3173. Make requests to `http://localhost:8000/health` to verify
318
319## Documentation
320
321- [Spikard Documentation](https://spikard.dev)
322- [PHP PSR Standards](https://www.php-fig.org/)
323"
324 )
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_php_scaffolder_generates_composer_json() {
334 let scaffolder = PhpScaffolder;
335 let content = scaffolder.generate_composer_json("test-app");
336
337 assert!(content.contains("\"your-vendor/test-app\""));
338 assert!(content.contains("\"php\": \"^8.2\""));
339 assert!(content.contains("\"spikard/spikard\": \"^"));
340 assert!(content.contains("\"psr-4\""));
341 }
342
343 #[test]
344 fn test_php_scaffolder_generates_phpstan_config() {
345 let scaffolder = PhpScaffolder;
346 let content = scaffolder.generate_phpstan_neon();
347
348 assert!(content.contains("level: max"));
349 assert!(content.contains("- src"));
350 assert!(content.contains("- tests"));
351 }
352
353 #[test]
354 fn test_php_scaffolder_generates_php_files_with_strict_types() {
355 let scaffolder = PhpScaffolder;
356 let app_content = scaffolder.generate_app_php();
357
358 assert!(app_content.starts_with("<?php"));
359 assert!(app_content.contains("declare(strict_types=1);"));
360 assert!(app_content.contains("namespace App;"));
361
362 let test_content = scaffolder.generate_app_test_php();
363 assert!(test_content.starts_with("<?php"));
364 assert!(test_content.contains("declare(strict_types=1);"));
365 assert!(test_content.contains("namespace App\\Tests;"));
366 }
367
368 #[test]
369 fn test_php_scaffolder_next_steps() {
370 let scaffolder = PhpScaffolder;
371 let steps = scaffolder.next_steps("my-project");
372
373 assert_eq!(steps.len(), 3);
374 assert!(steps[0].contains("cd my-project"));
375 assert_eq!(steps[1], "composer install");
376 assert_eq!(steps[2], "php bin/server.php");
377 }
378}