Cover classes with documentation
-
-
Save roxblnfk/2a9539aa79dc4c32727c79038d4a3236 to your computer and use it in GitHub Desktop.
$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' | |
prompts: | |
- id: cover-class-by-docs | |
description: Generate documentation for a specific class in the project | |
schema: | |
properties: | |
class-name: | |
description: Class name to cover with docs | |
required: | |
- class-name | |
messages: | |
- role: user | |
content: | | |
Read {{class-name}} class(es) or interface(s). | |
Cover it with comments using following important instructions: | |
- Don't break signatures. You can only add or improve comments. | |
- Don't remove existing @internal, @link, @see, @api, or @deprecated annotations. | |
- Use passive forms instead of we or you. | |
- Separate title and content with empty line. | |
- Use @link annotation if it's necessary to write some URL. | |
- Use @see annotation if it's necessary to reference to a class or method. | |
- Use extended types like class-string, non-empty-string, non-empty-array, non-empty-list, etc, if it's logically correct. | |
- Don't use @inheritDoc annotations. | |
- Keep docs clean: | |
- If the method has the same signature in the parent, don't comment the implementation if there is no additional things. | |
- Keep only essential and non-obvious documentation. It's important to avoid redundancy. | |
- remove trailing spaces even in empty comment lines | |
- Use generic annotations if possible based on the logic. For example for a `count()` has `int<0, max>` return type because the amount of items can't be negative. | |
- Use inline annotations like {@see ClassName} if it needed to mention in a text block. | |
- Add class or method usage example using markdown like. | |
Note that there is leading space before inside the code block: | |
```php | |
// Comment for the following code | |
some code | |
``` | |
If commenting a console command, feel fre to provide a usage example in the form of a command line: | |
```bash | |
# Comment for the following command | |
some command | |
``` | |
Don't skip internal classes. | |
- Inner comments order: title, description, example, template annotations, params and return annotations, references and internal/deprecated annotations. | |
- Property comments rules: If it possible, write inline comment without title like `/** @var non-empty-string $name User name */`. | |
- Break long lines into multiple lines. Maximum line length is 120 characters. | |
Multiline comments rules: if a comment starts with an annotation, the second line should start with whitespaces aligned with the annotation length: | |
```php | |
/** | |
* @var non-empty-string $name Comment on the first line | |
* comment on the second line... | |
* comment on the third line | |
*/ | |
``` |
This document outlines key practices for generating high-quality PHP code using LLMs, ensuring code remains maintainable, efficient, and follows modern PHP standards.
- Use constructor property promotion
- Leverage named arguments
- Implement match expressions instead of switch statements
- Use union types and nullable types
- Apply return type declarations consistently
- Use null coalescing operators (
??
,??=
) instead of ternary checks// Preferred $username = $request->username ?? 'guest'; // Avoid $username = isset($request->username) ? $request->username : 'guest';
- Use throw expressions with null coalescing for concise error handling
// Preferred $user = $orm->get('user') ?? throw new NotFoundException(); // Avoid $user = $orm->get('user'); if ($user === null) { throw new NotFoundException(); }
- Prefer attributes over PHPDoc annotations whenever possible
// Preferred - Using attributes #[AsController] #[Route('/api/users')] class UserController { #[Route('/get/{id}', methods: ['GET'])] public function getUser(#[FromRoute] int $id): User { // ... } } // Avoid - Using annotations /** * @Controller * @Route("/api/users") */ class UserController { /** * @Route("/get/{id}", methods={"GET"}) */ public function getUser(int $id): User { // ... } }
- Follow PER-2 coding standards which extends PSR-12
- Maintain single responsibility principle
- Keep methods focused and concise (under 20 lines when possible)
- Favor composition over inheritance
- Use strict typing (
declare(strict_types=1);
) - Declare classes as
final
by default, only omit when inheritance is actually needed// Preferred - Final by default final class UserRepository { // ... } // Only when needed for inheritance abstract class AbstractRepository { // ... }
- Prefer early returns to reduce nesting and improve readability
- Merge similar conditionals with the same action
// Preferred: Merged conditionals public function processUser(?User $user): bool { if ($user === null or !$user->isActive()) { return false; } // Process the valid user return true; } // Avoid: Repetitive conditionals public function processUser(?User $user): bool { if ($user === null) { return false; } if (!$user->isActive()) { return false; } // Process the valid user return true; }
- Use logical operators for compact conditional execution
// Preferred $condition and doSomething(); // Avoid if ($condition) { doSomething(); } // Preferred - using 'or' instead of 'not' with 'and' $skipAction or doAction(); // Avoid !$skipAction and doAction();
- Prefer ternary operator for simple conditional assignments and returns
// Preferred return $condition ? $foo : $bar; // Avoid if ($condition) { return $foo; } return $bar;
-
Use enums instead of class constants for representing a fixed set of related values
-
Use CamelCase for enum case names as per PER-2 standard
// Preferred - Using an enum with CamelCase cases enum Status { case Pending; case Processing; case Completed; case Failed; } // Avoid - Using class constants class Status { public const PENDING = 'pending'; public const PROCESSING = 'processing'; public const COMPLETED = 'completed'; public const FAILED = 'failed'; }
-
Use backed enums when you need primitive values (strings/integers) for cases
enum Status: string { case Pending = 'pending'; case Processing = 'processing'; case Completed = 'completed'; case Failed = 'failed'; } // Usage with database or API $status = Status::Completed; $database->updateStatus($id, $status->value); // 'completed'
-
Add methods to enums to encapsulate related behavior
enum PaymentStatus: string { case Pending = 'pending'; case Paid = 'paid'; case Refunded = 'refunded'; case Failed = 'failed'; public function isSuccessful(): bool { return $this === self::Paid || $this === self::Refunded; } public function canBeRefunded(): bool { return $this === self::Paid; } public function getLabel(): string { return match($this) { self::Pending => 'Awaiting Payment', self::Paid => 'Payment Received', self::Refunded => 'Payment Refunded', self::Failed => 'Payment Failed', }; } } // Usage $status = PaymentStatus::Paid; if ($status->canBeRefunded()) { // Process refund }
-
Implement interfaces with enums to enforce contracts
interface ColorInterface { public function getRgb(): string; } enum Color implements ColorInterface { case Red; case Green; case Blue; public function getRgb(): string { return match($this) { self::Red => '#FF0000', self::Green => '#00FF00', self::Blue => '#0000FF', }; } }
-
Use static methods for converting from and to enum cases
enum Status: string { case Pending = 'pending'; case Processing = 'processing'; case Completed = 'completed'; case Failed = 'failed'; public static function fromDatabase(?string $value): ?self { if ($value === null) { return null; } return self::tryFrom($value) ?? throw new \InvalidArgumentException("Invalid status: {$value}"); } }
-
Use enums in type declarations
function processOrder(Order $order, Status $status): void { match($status) { Status::Pending => $this->queueOrder($order), Status::Processing => $this->notifyProcessing($order), Status::Completed => $this->markComplete($order), Status::Failed => $this->handleFailure($order), }; }
- Prefer immutable objects and value objects where appropriate
- Use readonly properties for immutable class properties
final class UserId { public function __construct( public readonly string $value, ) {} }
- Use the
with
prefix for methods that return new instances with modified valuesfinal class User { public function __construct( public readonly string $name, public readonly string $email, public readonly \DateTimeImmutable $createdAt, ) { } // Returns new instance with modified name public function withName(string $name): self { return new self( $name, $this->email, $this->createdAt, ); } // Returns new instance with modified email public function withEmail(string $email): self { return new self( $this->name, $email, $this->createdAt, ); } } // Usage $user = new User('John', '[email protected]', new \DateTimeImmutable()); $updatedUser = $user->withName('Jane')->withEmail('[email protected]');
- Favor constructor injection for dependencies
final class UserService { public function __construct( private readonly UserRepositoryInterface $userRepository, private readonly LoggerInterface $logger, ) {} }
- Define interfaces for services to allow for different implementations
- Avoid service locators and static method calls for dependencies
- Use dependency injection containers for wiring services together
- Keep classes focused on their responsibility; don't inject unnecessary dependencies
- Use extended type annotations in PHPDoc for more precise type definitions
/** * @param non-empty-string $id The user ID * @param list<Role> $roles List of user roles * @return array<string, mixed> */ public function getUserData(string $id, array $roles): array { // ... }
- Leverage generics in collections and repositories
/** * @template T of object */ interface RepositoryInterface { /** * @param class-string<T> $className * @param non-empty-string $id * @return T|null */ public function find(string $className, string $id): ?object; /** * @param T $entity * @return void */ public function save(object $entity): void; }
- Use precise numeric range types when applicable
/** * @param int<0, max> $limit * @param int<0, max> $offset * @return list<User> */ public function getUsers(int $limit, int $offset): array { // ... }
- Use exceptions for error conditions
- Prefer typed exceptions for specific error categories
- Avoid suppressing errors with
@
- Include meaningful error messages
- Avoid using
empty()
function; use explicit comparisons instead- Use
$array === []
instead ofempty($array)
orcount($array) === 0
- Use
$string === ''
instead ofempty($string)
- Use
- Prefer array functions like
array_filter()
,array_map()
, andarray_reduce()
- Use
$value === null
instead ofis_null($value)
for null checks - Use strict equality (
===
) instead of loose equality (==
) - Use
isset()
only for checking if variables or properties are defined, not for null comparison// Correct use of isset() if (isset($data['key'])) { // Checks if array key exists and not null // Use $data['key'] } // Incorrect use of isset() if (isset($definedVariable)) { // Don't use isset() when variable is definitely defined // Instead use $definedVariable !== null }
- Use null coalescing operator for default values
// Preferred $config = $options['config'] ?? []; // Avoid $config = isset($options['config']) ? $options['config'] : [];
- Sanitize all user inputs
- Parameterize database queries
- Avoid using
eval()
or other dynamic code execution - Implement proper authentication and authorization checks
[To be extended with code examples]
This document outlines the standards and best practices for writing unit tests for the project.
- Unit tests should be in
tests/Unit
, integration tests intests/Integration
, acceptance tests intests/Acceptance
, architecture tests intests/Arch
, etc. - Tests should be organized to mirror the project structure.
- Each concrete PHP class should have a corresponding test class
- Place tests in the appropriate namespace matching the source code structure
- Use
final
keyword for test classes
Source: src/ExternalContext/LocalExternalContextSource.php
Test: tests/Unit/ExternalContext/LocalExternalContextSourceTest.php
- Test Concrete Implementations, Not Interfaces: Interfaces define contracts but don't contain actual logic to test. Only test concrete implementations of interfaces.
- Focus on Behavior: Test the behavior of classes rather than their internal implementation details.
- Test Public API: Focus on testing the public methods and functionality that clients of the class will use.
- Test Edge Cases: Include tests for boundary conditions, invalid inputs, and error scenarios.
Modules located in src/Module
are treated as independent units with their own test structure:
-
Each module should have its own test directory with the following structure:
tests/Unit/Module/{ModuleName}/ ├── Stub/ (Contains stubs for the module's dependencies) └── Internal/ (Tests for module's internal implementations)
-
Internal implementations of a module are located in the
Internal
folder of each module -
Tests for public classes of the module should be placed directly in the module's test directory corresponding to the source code structure
-
Each module's tests should be structured as independent areas with their own stubs
All tests should follow the AAA pattern:
- Arrange: Set up the test environment and prepare inputs
- Act: Execute the code being tested
- Assert: Verify the results are as expected
Use comments to separate these sections for clarity:
public function testFetchContextReturnsDecodedJson(): void
{
// Arrange
$filePath = '/path/to/context.json';
$fileContent = '{"key":"value"}';
$this->fileSystem->method('exists')->willReturn(true);
$this->fileSystem->method('readFile')->willReturn($fileContent);
// Act
$result = $this->contextSource->fetchContext($filePath);
// Assert
self::assertSame(['key' => 'value'], $result);
}
- Test classes should be named with the pattern
{ClassUnderTest}Test
- Test methods should follow the pattern
test{MethodName}{Scenario}
- Use descriptive method names that explain what is being tested
#[CoversClass(LocalExternalContextSource::class)]
final class LocalExternalContextSourceTest extends TestCase
{
public function testFetchContextReturnsValidData(): void
{
// Test implementation
}
public function testFetchContextThrowsExceptionWhenFileNotFound(): void
{
// Test implementation
}
}
- Use strict typing:
declare(strict_types=1);
- Use namespaces consistent with the project structure
- Extend
PHPUnit\Framework\TestCase
- Use assertion methods with descriptive error messages
- Test one behavior per test method
- Use data providers with PHP 8.1+ attributes and generators
#[DataProvider('provideValidFilePaths')]
public function testFetchContextWithVariousPaths(string $path, array $expectedData): void
{
// Arrange
$this->fileSystem->method('exists')->willReturn(true);
$this->fileSystem->method('readFile')->willReturn(\json_encode($expectedData));
// Act
$result = $this->contextSource->fetchContext($path);
// Assert
self::assertSame($expectedData, $result);
}
public static function provideValidFilePaths(): Generator
{
yield 'relative path' => ['relative/path.json', ['expected' => 'data']];
yield 'absolute path' => ['/absolute/path.json', ['expected' => 'data']];
}
- Each test should be independent of others
- Use setUp() and tearDown() methods for common test preparation and cleanup
- Use test doubles (mocks, stubs) for isolating the code under test from dependencies
- Reset global/static state between tests
- For module tests, use the dedicated Stub directory to store all stub implementations
protected function setUp(): void
{
// Arrange (common setup)
$this->fileSystem = $this->createMock(FileSystemInterface::class);
$this->contextSource = new LocalExternalContextSource($this->fileSystem);
}
When testing modules from src/Module
:
- Module tests should use stubs from their dedicated
Stub
directory - Tests should only rely on the public interfaces of the module, not internal implementations
- Internal tests can have additional stubs specific to internal components
- Cross-module dependencies should be stubbed if possible (for interfaces), treating each module as an independent unit
// Example of module test setup with stubs
namespace Tests\Unit\Module\Payment;
use Tests\Unit\Module\Payment\Stub\PaymentGatewayStub;
use Tests\Unit\Module\Payment\Stub\LoggerStub;
final class PaymentProcessorTest extends TestCase
{
private PaymentGatewayStub $paymentGateway;
private LoggerStub $logger;
protected function setUp(): void
{
$this->paymentGateway = new PaymentGatewayStub();
$this->logger = new LoggerStub();
$this->processor = new PaymentProcessor($this->paymentGateway, $this->logger);
}
}
- Use specific assertion methods instead of generic ones
- Provide meaningful failure messages in assertions
- Test both positive and negative scenarios
- Assert state changes and side effects, not just return values
// Good
self::assertSame('expected', $actual, 'Context data should match the expected format');
// Instead of
self::assertTrue($expected === $actual);
When testing exceptions, the AAA pattern is slightly modified:
public function testFetchContextThrowsExceptionWhenFileNotFound(): void
{
// Arrange
$filePath = '/non-existent/path.json';
$this->fileSystem->method('exists')->willReturn(false);
// Assert (before Act for exceptions)
$this->expectException(ContextSourceException::class);
$this->expectExceptionMessage('Cannot read context from file');
// Act
$this->contextSource->fetchContext($filePath);
}
- Only mock direct dependencies of the class under test
- Mock only what is necessary for the test
- DO NOT MOCK enums or final classes - this is strictly prohibited
- Prefer typed mock method returns
- Verify critical interactions with mocks
// Arrange
$this->fileSystem->expects(self::once())
->method('readFile')
->with('/path/to/file.json')
->willReturn('{"key": "value"}');
- Enumerations must not be mocked and need to be used as is - always use real enum instances in tests
- Final classes should not be mocked - use real instances or alternative approaches
When a class under test depends on a final class:
- Use real instances when possible - this is the preferred approach
- Create test doubles by implementing the same interface if the final class implements an interface
- Use wrapper/adapter pattern to create a non-final class that delegates to the final class if necessary
- Refactor dependencies to use interfaces where appropriate for improved testability
// Instead of mocking a final class:
final class FileReader
{
public function readFile(string $path): string {...}
}
// Create an interface:
interface FileReaderInterface
{
public function readFile(string $path): string;
}
// Create a test implementation:
class TestFileReader implements FileReaderInterface
{
public function readFile(string $path): string
{
return '{"test":"data"}';
}
}
// Use in tests:
$fileReader = new TestFileReader();
$myService = new MyService($fileReader);
For enums, always use the real enum values directly:
// When testing with an enum dependency:
enum Status: string
{
case PENDING = 'pending';
case APPROVED = 'approved';
case REJECTED = 'rejected';
}
// In your test - ALWAYS use the real enum instance:
public function testProcessWithPendingStatus(): void
{
// Arrange - use the real enum value
$status = Status::PENDING;
$processor = new StatusProcessor();
// Act
$result = $processor->process($status);
// Assert
self::assertTrue($result);
}
Replace annotations with attributes throughout your tests:
#[CoversClass(ClassUnderTest::class)]
- Specify which class is being tested#[CoversMethod('methodName')]
- Specify which method is being tested#[DataProvider('provideTestData')]
- Link to data provider method#[Group('slow')]
- Categorize tests#[TestDox('Class should handle errors gracefully')]
- Better test documentation
public function testBasicFunctionality(): SomeClass
{
// Arrange
$object = new SomeClass();
// Act & Assert
self::assertInstanceOf(SomeClass::class, $object);
return $object;
}
#[Depends('testBasicFunctionality')]
public function testAdvancedFeature(SomeClass $object): void
{
// Arrange is handled by the dependency
// Act
$result = $object->advancedMethod();
// Assert
self::assertTrue($result);
}
Use traits to share test functionality:
trait CreatesTempFilesTrait
{
private string $tempFilePath;
protected function createTempFile(string $content): string
{
// Arrange test environment
$this->tempFilePath = sys_get_temp_dir() . '/' . uniqid('test_', true);
file_put_contents($this->tempFilePath, $content);
return $this->tempFilePath;
}
protected function tearDown(): void
{
// Clean up test environment
if (isset($this->tempFilePath) && file_exists($this->tempFilePath)) {
unlink($this->tempFilePath);
}
parent::tearDown();
}
}
final class MyTest extends TestCase
{
use CreatesTempFilesTrait;
public function testFileProcessing(): void
{
// Arrange
$path = $this->createTempFile('{"data":"value"}');
// Act
$result = $this->processor->process($path);
// Assert
self::assertNotEmpty($result);
}
}