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