Skip to main content

spikard_cli/init/
php.rs

1//! PHP Project Scaffolder
2//!
3//! Generates a minimal PHP project structure with Spikard integration.
4//! Follows PSR-4 autoloading conventions and modern PHP 8.2+ standards.
5
6use super::scaffolder::{ProjectScaffolder, ScaffoldedFile};
7use anyhow::Result;
8use std::path::{Path, PathBuf};
9
10/// PHP project scaffolder
11pub 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}