Skip to content

Instantly share code, notes, and snippets.

@roxblnfk
Last active April 14, 2025 22:19
Show Gist options
  • Save roxblnfk/2a9539aa79dc4c32727c79038d4a3236 to your computer and use it in GitHub Desktop.
Save roxblnfk/2a9539aa79dc4c32727c79038d4a3236 to your computer and use it in GitHub Desktop.

Cover classes with documentation

$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
*/
```

PHP Best Practices for LLM Code Generation

Overview

This document outlines key practices for generating high-quality PHP code using LLMs, ensuring code remains maintainable, efficient, and follows modern PHP standards.

Core Principles

1. Modern PHP Features (PHP 8.1+)

  • 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
        {
            // ...
        }
    }

2. Code Structure

  • 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;

3. Enumerations (PHP 8.1+)

  • 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),
        };
    }

4. Immutability and Value Objects

  • 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 values
    final 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]');

5. Dependency Injection and IoC

  • 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

6. Type System and Generics

  • 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
    {
        // ...
    }

7. Error Handling

  • Use exceptions for error conditions
  • Prefer typed exceptions for specific error categories
  • Avoid suppressing errors with @
  • Include meaningful error messages

8. Array and Collection Handling

  • Avoid using empty() function; use explicit comparisons instead
    • Use $array === [] instead of empty($array) or count($array) === 0
    • Use $string === '' instead of empty($string)
  • Prefer array functions like array_filter(), array_map(), and array_reduce()

9. Comparison and Null Checks

  • Use $value === null instead of is_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'] : [];

10. Security Considerations

  • Sanitize all user inputs
  • Parameterize database queries
  • Avoid using eval() or other dynamic code execution
  • Implement proper authentication and authorization checks

Implementation Examples

[To be extended with code examples]

PHP Unit Testing Guidelines

This document outlines the standards and best practices for writing unit tests for the project.

Test Structure

  • Unit tests should be in tests/Unit, integration tests in tests/Integration, acceptance tests in tests/Acceptance, architecture tests in tests/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

What to Test

  • 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.

Module Testing

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

Arrange-Act-Assert (AAA) Pattern

All tests should follow the AAA pattern:

  1. Arrange: Set up the test environment and prepare inputs
  2. Act: Execute the code being tested
  3. 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);
}

Naming Conventions

  • 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
    }
}

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']];
}

Test Isolation

  • 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);
}

Module-Specific Test Isolation

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);
    }
}

Assertions

  • 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);

Error Handling Tests

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);
}

Mock Objects

  • 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"}');

Dealing with Final Classes and Enums

  • 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:

  1. Use real instances when possible - this is the preferred approach
  2. Create test doubles by implementing the same interface if the final class implements an interface
  3. Use wrapper/adapter pattern to create a non-final class that delegates to the final class if necessary
  4. 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);
}

Additional Modern PHPUnit Features

PHP 8.1+ Attributes

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

Using depends with Attributes

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);
}

Test Extension with Traits

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);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment