diff --git a/.cursor/cli.json b/.cursor/cli.json new file mode 100644 index 00000000..aa2a1431 --- /dev/null +++ b/.cursor/cli.json @@ -0,0 +1,6 @@ +{ + "permissions": { + "allow": [], + "deny": ["Shell(git push)", "Shell(gh pr create)", "Write(**)"] + } +} diff --git a/.cursor/commands/refactor.md b/.cursor/commands/refactor.md new file mode 100644 index 00000000..fec65c22 --- /dev/null +++ b/.cursor/commands/refactor.md @@ -0,0 +1 @@ +Refactor following our minimalist code philosophy then organize and catalog like a librarian and obsess over code consistency. Let's take this code from an A+ to an A++ πŸš€ diff --git a/.cursor/commands/review.md b/.cursor/commands/review.md new file mode 100644 index 00000000..988ab9b8 --- /dev/null +++ b/.cursor/commands/review.md @@ -0,0 +1 @@ +Analyze and meticulously catalog all the changes in this branch, including all the changes that haven't been committed yet. Report back on where the changes fall short of our development, architecture and testing rules. diff --git a/.cursor/rules/00-main.mdc b/.cursor/rules/00-main.mdc index 42c9ca29..398bec98 100644 --- a/.cursor/rules/00-main.mdc +++ b/.cursor/rules/00-main.mdc @@ -6,6 +6,8 @@ alwaysApply: true We're developing Deployer PHP, a Composer package and CLI tool that simplifies provisioning and setting up servers across multiple Cloud providers, enabling you to deploy your PHP projects effortlessly. +**🚨 All our rules are MANDATORY and IMPORTANT!** + **πŸ”Ž Before ANY Task:** - Check `composer.json` and `package.json` for installed packages @@ -18,6 +20,18 @@ We're developing Deployer PHP, a Composer package and CLI tool that simplifies p - Always ask yourself if less code can achieve the same result - Refactor relentlessly for code clarity and necessity +**πŸ€“ Organize & Catalog Like A Librarian:** + +Organize and catalog code like a librarian. Group related functions into comment-separated sections for visual clarity and prefer alphabetical ordering when it doesn’t fight logical grouping. Code should be functional and visually appealing; there is beauty in order. + +**πŸ€– Obsessive Code Consistency:** + +Be rigorous to the point of perfectionism in maintaining implementation consistency and logical coherence across similar areas of the codebase. Additionally, make it a habit to regularly review other code in and around the files you are working on to identify reusable patterns. + +The goal is for all the code in this repository to appear as if it were written by a single individual, adhering to a uniform style, a consistent set of standards, and aesthetic principles in software development. + +This can include everything from naming files, classes, variables, or array keys to the precedence and type of parameters passed to a function, to how logic flows and how the code is organized or commented. + **βœ”οΈ Don't worry about tests:** Write or run tests ONLY if specifically instructed **🧠 AI Agent Protocol:** ULTRATHINK β†’ STEP BY STEP β†’ ACT diff --git a/.cursor/rules/01-architecture.mdc b/.cursor/rules/01-architecture.mdc index eeb85f4e..0963792a 100644 --- a/.cursor/rules/01-architecture.mdc +++ b/.cursor/rules/01-architecture.mdc @@ -1,15 +1,27 @@ --- -globs: app/**/*.php,templates/**/*.yaml +globs: app/**/*.php,tests/**/*.php +alwaysApply: false --- -### Architecture (MANDATORY) +## Architecture Rules -- **PHP:** PSR-12, strict types, PHP 8.x features (unions, match, attributes, readonly); -- **Imports:** Always add `use` statements instead of fully qualified class names; -- **Type Safety:** All methods must have explicit return types with proper generics (e.g., `Collection`). +- **PHP:** PSR-12, strict types, PHP 8.x features (unions, match, attributes, readonly) +- **Imports:** Always add `use` statements instead of fully qualified class names +- **Type Safety:** All methods must have explicit return types with proper generics (e.g., `Collection`) **🚨 Architecture rules are IMMUTABLE - fix violating code, not the architecture rules** +### Symfony Patterns + +- **Dependency Injection:** instead of manually resolving and instantiating classes +- **Symfony Classes:** instead of native PHP functions for easier mocking during testing (eg. `Filesystem::`, `Process::`, etc.) + +### Dependency Injection + +- Dependencies are automatically injected by the `app/Container` class +- ALL dependencies MUST be injected through constructors - NO manual instantiation +- **Exceptions:** Only value objects, DTOs, and pure data structures can be manually instantiated + ### Command Layer - Commands handle user interaction (input/output) and orchestrate Services @@ -26,31 +38,58 @@ globs: app/**/*.php,templates/**/*.yaml - Services handle core business logic, external API calls, file operations - Complex orchestration shared by multiple Commands should be extracted to dedicated Services -### Shared Utilities - -- Common path resolution, configuration loading goes in dedicated utility classes -- Utility classes are static or singleton patterns for simple operations -- No business logic or state in utilities - pure functions only - ### Console I/O Rules - Only Commands perform console input/output operations -- Services return exceptions or structured data for Commands to handle - Use SymfonyStyle consistently for all user-facing output +- Services return exceptions or structured data for Commands to handle - Validation errors and business exceptions bubble up to Commands for display -### Dependency Injection (Mandatory) - -- ALL dependencies MUST be injected through constructors - NO manual instantiation -- Commands receive Services via constructor injection -- Services receive other Services/utilities via constructor injection -- Use ServiceContainer or dependency injection container for all object creation -- NEVER use `new ClassName()` inside methods - always inject dependencies -- Exceptions: Only value objects, DTOs, and pure data structures can be manually instantiated - ### Dependency Rules - Commands depend on Services - Services depend on other Services or utilities -- No circular dependencies allowed - All dependencies declared in constructor signatures + +**IMPORTANT:** No circular dependencies allowed. + +### Comments + +**DocBlock:** Add docblock comments with minimalist descriptions, parameters and return types for classes and functions; + +**Comment everything:** Use comments to separate sections and explain or summarize complex logic; + +``` +// +// {Section Header} +// ------------------------------------------------------------------------------- + +// +// {Section Subheader} + +// {Paragraph} +``` + +- Use comments as visual separators to help separate different sections +- Separate section headers, subheaders and paragraphs with a single newline +- Avoid commenting the obvious or leaving comments behind when removing code + +**ALWAYS use the correct section header comment format and not the simplified one:** + +``` + // + // {Section Header} + // ---- ❌ Too few dashes +``` + +### Quality Gates + +**ALWAYS run these commands against the files you have touched and fix any issues BEFORE considering a task complete:** + +```bash +vendor/bin/rector $CHANGED_PHP_FILES # Code improvements (changed files only) +vendor/bin/pint $CHANGED_PHP_FILES # Fix code style (changed files only) + +# Static analysis excluding tests (never do static analysis against tests) +vendor/bin/phpstan analyze $CHANGED_PHP_FILES_EXCEPT_TESTS +``` diff --git a/.cursor/rules/02-code-quality.mdc b/.cursor/rules/02-code-quality.mdc deleted file mode 100644 index e36e9152..00000000 --- a/.cursor/rules/02-code-quality.mdc +++ /dev/null @@ -1,35 +0,0 @@ ---- -globs: app/**/*.php,tests/**/*.php,templates/**/*.yaml ---- - -## Code Quality Rules - -**DocBlock:** Add docblock comments with minimalist descriptions, parameters and return types for classes and functions; - -**Comment everything:** Use comments to separate sections of code and explain or summarize complex logic; avoid commenting the obvious: - -``` -// -// {Section Header} -// ------------------------------------------------------------------------------- - -// -// {Section Subheader} - -// {Paragraph} -``` - -- Separate section headers, subheaders and paragraphs with a single newline; -- Avoid commenting the obvious or leaving comments behind when removing code. - -## Quality Gates - -**ALWAYS run these commands against the files you have touched and fix any issues BEFORE considering a task complete:** - -```bash -vendor/bin/rector $CHANGED_PHP_FILES # Code improvements (changed files only) -vendor/bin/pint $CHANGED_PHP_FILES # Fix code style (changed files only) - -# Static analysis excluding tests (never do static analysis against tests) -vendor/bin/phpstan analyze $CHANGED_PHP_FILES_EXCEPT_TESTS -``` diff --git a/.cursor/rules/02-tests.mdc b/.cursor/rules/02-tests.mdc new file mode 100644 index 00000000..c64dcc2c --- /dev/null +++ b/.cursor/rules/02-tests.mdc @@ -0,0 +1,102 @@ +--- +globs: app/**/*.php,tests/**/*.php +--- + +## Testing Rules + +**Philosophy:** "A test that never fails is not a test, it's a lie." + +**Framework:** Pest exclusively with `it()` syntax, 80%+ coverage + +### Running Tests + +- `composer pest` - run entire test suite in parallel, with coverage +- `vendor/bin/pest $TEST_FILE` - run specific test file + +### Test Minimalism + +**Target:** Keep test files under 1.8x the size of source code they test. + +**Rules:** + +- Test core business logic only, skip framework testing +- Use dataset-driven testing: `->with([])` for multiple scenarios +- Eliminate test overlap: no two tests covering same functionality +- Consolidate assertions: `expect($x)->toBe(1)->and($y)->toBe(2)` +- Mock external dependencies only +- No performance tests unless performance is the primary concern +- Don't sacrifice readability for ratio targets + +**Don't consolidate when:** + +- Different public methods +- Exception vs normal flow tests +- Different setup requirements +- Distinct business logic + +### AAA Pattern (MANDATORY) + +```php +it('does something specific', function () { + // ARRANGE + $service = new Service(mock(Dependency::class)); + + // ACT + $result = $service->performAction(); + + // ASSERT + expect($result)->toBe('expected'); + + // CLEANUP (when needed) + $this->resetTimeState(); + unlink($tempFile); +}); +``` + +**Exception tests:** Use `// ACT & ASSERT` when act triggers assertion. + +**Organization:** Use `describe()` blocks, `beforeEach()` setup, extract helpers/traits for DRY tests. + +### Testing Patterns + +**❌ FORBIDDEN:** + +```php +expect($x)->toBeInstanceOf(Class::class); // Type-only testing +expect($x)->toBeArray(); // Generic assertions +expect($x)->not->toBeNull(); // Meaningless +expect(true)->toBeTrue(); // Literally meaningless +sleep(...); // Use time mocking +``` + +**βœ… REQUIRED:** + +```php +expect($config->getValue('host'))->toBe('example.com'); +expect($this->validator->isValid($input))->toBe($expected); +$mock->shouldReceive('method')->with('param')->andReturn('result'); +``` + +### Test Types + +**Unit Tests:** + +- Mock all external dependencies (filesystem, HTTP, processes) +- Test single units in isolation +- Complete in milliseconds + +**Integration Tests:** + +- Real file operations and external processes +- CLI commands and full workflows + +**Layer Strategy:** + +- CLI Commands β†’ Integration tests +- Business Services β†’ Unit tests (mocked dependencies) +- Utilities/Helpers β†’ Unit tests + +### Static Analysis + +- Ignore PHPStan issues in tests - focus on test functionality over compliance +- Avoid excessive phpdoc just to appease types diff --git a/.cursor/rules/03-tests.mdc b/.cursor/rules/03-tests.mdc deleted file mode 100644 index a5747e8d..00000000 --- a/.cursor/rules/03-tests.mdc +++ /dev/null @@ -1,221 +0,0 @@ ---- -globs: app/**/*.php,tests/**/*.php ---- - -## PHP CLI Testing Rules - -**Philosophy:** "A test that never fails is not a test, it's a lie." - -**Framework:** Pest exclusively with `it()` syntax, 60%+ coverage - -### Running Tests - -- `composer pest` run the entire test suite in parallel, with coverage -- `vendor/bin/pest $TEST_FILE` run a specific test file - -### Test Minimalism Rules - -**Write only essential tests that would break if business logic fails:** - -- **Core Business Logic Only:** Test critical paths, skip framework testing -- **Minimal Test Data:** Use simplest possible setup, avoid complex scenarios -- **Standard Assertions:** Prefer built-in `expect()` over custom assertions -- **Essential Edge Cases:** Only test failure modes that actually matter -- **No Performance Tests:** Unless performance is the primary concern - -### Static Analysis in Tests - -- Ignore PHPStan issues in tests. -- Avoid excessive phpdoc just to appease types; add only when necessary. - -### AAA Pattern Requirements (MANDATORY) - -**All tests MUST follow the AAA pattern with explicit section headers:** - -```php -it('does something specific', function () { - // ARRANGE - $testData = ['key' => 'value']; - $mockService = mock(SomeService::class); - - // ACT - $result = $this->service->performAction($testData); - - // ASSERT - expect($result)->toBe($expectedValue); - $mockService->shouldHaveReceived('method'); - - // CLEANUP (when needed) - $this->resetTimeState(); - $this->cleanupTempFiles(); -}); -``` - -**Required Section Headers:** - -- `// ARRANGE` - Setup test data, mocks, and dependencies -- `// ACT` - Execute the code under test (single action) -- `// ASSERT` - Verify expected outcomes and behaviors -- `// CLEANUP` - Reset state when necessary (time state, temp files, etc.) - -**Exception Pattern:** - -- `// ACT & ASSERT` - For exception tests where the act triggers the assertion - -### Organizing Tests & Helpers - -**DRY Principle:** Tests should be as DRY and streamlined as the code they're testing. - -**Best Practices:** - -- Logical grouping with `describe()` blocks -- Extract repeated mocking into reusable helper methods -- Create test traits for shared behavior across test classes -- Use `beforeEach()` blocks for common setup within test groups -- Use proper cleanup in tests (temp files, reset state) for test isolation -- Build helper functions for creating test configurations and mock data - -#### Test Helpers - -- Evolution-friendly, loosely coupled -- Support interface changes - -**Helper Examples:** - -```php -// Test trait for common mocking -trait MocksExternalServices -{ - protected function mockSuccessfulProcess(): void - { - // Mock external process calls - $this->processRunner = mock(ProcessRunner::class); - $this->processRunner->shouldReceive('run') - ->andReturn(['output' => 'Success', 'exitCode' => 0]); - } -} - -// Reusable test data builders -function createCommandOptions(array $overrides = []): array -{ - return array_merge([ - 'composer' => true, - 'npm' => false, - 'force' => false, - ], $overrides); -} - -// beforeEach for common setup -describe('package installation', function () { - beforeEach(function () { - $this->mockSuccessfulProcess(); - $this->baseOptions = createCommandOptions(); - }); -}); -``` - -**Benefits:** - -- Reduces test maintenance burden -- Ensures consistent mocking patterns -- Makes tests more readable and focused -- Easier to update when dependencies change - -### FORBIDDEN Patterns (Auto-Reject) - -```php -expect($x)->toBeInstanceOf(Class::class); // Type-only -expect($x)->toBeArray(); // Generic -expect($x)->not->toBeNull(); // Meaningless -expect($x)->toBeTrue(); // No context -expect(true)->toBeTrue(); // This is literally meaningless -expect($object->property())->toBeInstanceOf(); // Property type testing -sleep(...); // Use proper time mocking instead -``` - -### REQUIRED Patterns - -```php -// Test specific values and behavior -expect($config->getValue('host'))->toBe('example.com') - ->and($config->getValue('port'))->toBe(22); - -// Test with datasets -it('validates server hostnames', function (string $hostname, bool $valid) { - expect($this->validator->isValidHostname($hostname))->toBe($valid); -})->with([ - ['server.example.com', true], - ['invalid_hostname!', false], -]); - -// Mock only external dependencies -$processRunner = mock(ProcessRunner::class); -$processRunner->shouldReceive('run')->with('ls *')->andReturn('file1.txt'); - -$httpClient = mock(HttpClient::class); -$httpClient->shouldReceive('get')->with('api/endpoint')->andReturn(['status' => 'ok']); -``` - -### Unit Test Isolation - -**Core Principle:** True unit tests must be isolated from external dependencies. - -**Unit tests should:** - -- Test single units of code in isolation -- Use mocks/fakes for all external dependencies -- Run without external dependencies -- Complete in milliseconds - -**Unit tests MUST NOT:** - -- Use real file system, network calls, or shell commands -- Test service integrations with external systems -- Make actual HTTP requests or process executions -- Depend on external services, APIs, or system processes - -**Integration vs Unit:** Use integration tests for file system operations and external processes, unit tests (with mocks) for pure business logic. - -### Layer Testing Strategy - -- **CLI Commands:** Integration tests, mock external processes and services -- **Business Services:** Unit tests, mock all external calls and file operations -- **Utilities/Helpers:** Unit tests with isolated scenarios - -```php -// βœ… Service unit test (mocked dependencies) -$this->fileSystem->shouldReceive('exists')->andReturn(true); -$this->processRunner->shouldReceive('execute')->andReturn(['output' => 'success']); - -// βœ… Command integration test (real file operations) -$testFile = '/tmp/test-config.yml'; -file_put_contents($testFile, 'host: example.com'); -$result = $this->command->execute(['--config' => $testFile]); -expect($result->exitCode)->toBe(0); -unlink($testFile); // cleanup -``` - -### Performance Testing - -**When testing CLI operations:** - -- Monitor execution time for file operations and external processes -- Test with realistic server counts and configuration sizes to expose performance issues -- Verify efficient processing of multiple servers or long-running deployment operations - -```php -// βœ… CLI performance monitoring pattern -it('processes server provisioning efficiently', function () { - // ARRANGE - $serverConfigs = createTestServerConfigs(5); - - // ACT - $startTime = microtime(true); - $result = $this->provisioningService->processServers($serverConfigs); - $executionTime = microtime(true) - $startTime; - - // ASSERT - expect($result->getSuccessCount())->toBe(5) - ->and($executionTime)->toBeLessThan(2.0); // Should complete provisioning in under 2 seconds -}); -```