guidelines.md 9.4 KB

AP2I Project Guidelines

Project Overview

AP2I is a Symfony 7.3-based API application that is part of the OpenTalent ecosystem. It's a comprehensive API platform built with modern PHP (8.2+) and leverages API Platform 4.1 for REST/GraphQL API capabilities.

Key Technologies

  • Framework: Symfony 7.3
  • API Platform: 4.1 (REST/GraphQL APIs)
  • PHP: 8.2+ (strict requirement)
  • ORM: Doctrine 3.3 with migrations
  • Authentication: JWT (Lexik JWT Authentication Bundle)
  • Database: Uses Doctrine DBAL 3.9 and a mariadb 10.4.26 DB
  • Messaging: Symfony Messenger for async processing
  • Testing: PHPUnit 9.6
  • Code Quality: PHPStan, PHP CS Fixer

Business Domains

The application handles multiple business domains including:

  • Organization management
  • Education systems
  • Billing and payments
  • User profiles and access control
  • Network management
  • Booking systems
  • Export functionality
  • Shop/e-commerce features

Project Structure

src/
├── Message/           # Symfony Messenger messages and handlers
├── DataFixtures/      # Database fixtures for testing/development
├── Validator/         # Custom validation logic
├── Enum/              # Domain-specific enumerations
├── Service/           # Business logic services
└── ...               # Other domain-specific directories

tests/
├── Fixture/          # Test data fixtures and factories
├── Application/      # Application-level integration tests
├── Unit/            # Unit tests organized by domain
└── ...

config/               # Symfony configuration
public/              # Web root directory
templates/           # Twig templates
migrations/          # Doctrine database migrations

Development Guidelines

Testing

  • Always run tests before submitting changes to ensure correctness
  • The project uses PHPUnit for testing with comprehensive unit and application tests
  • Tests are organized by domain to mirror the application structure
  • Do not modify the SUT, except for converting private methods into protected ones, or because there is an obvious error in it
  • Take tests/Unit/Service/Typo3/Typo3ServiceTest.php as the reference example for writing service tests

Test Class Structure

Each test class should follow this structure:

<?php

namespace App\Tests\Unit\Service\MyDomain;

use App\Service\MyDomain\MyService;
use PHPUnit\Framework\TestCase;
use SomeDependency\Interface;
use PHPUnit\Framework\MockObject\MockObject;

// Create testable class for protected methods access
class TestableMyService extends MyService
{
    public function protectedMethod(): mixed
    {
        return parent::protectedMethod();
    }
}

class MyServiceTest extends TestCase
{
    private Interface|MockObject $dependency;

    public function setUp(): void
    {
        // Mock all dependencies in setUp
        $this->dependency = $this->getMockBuilder(Interface::class)
            ->disableOriginalConstructor()
            ->getMock();
    }

    private function getMyServiceMockFor(string $methodName): TestableMyService|MockObject
    {
        return $this->getMockBuilder(TestableMyService::class)
            ->setConstructorArgs([$this->dependency])
            ->setMethodsExcept([$methodName])
            ->getMock();   
    }
}

Mocking Strategy

  • Use setMethodsExcept() to mock all methods except the one being tested (ignore deprecation warnings)
  • Create a dedicated mock factory method following the pattern getMyClassMockFor(string $methodName)
  • Mock factory method should:
    • Create the SUT mock using getMockBuilder()
    • Inject dependency mocks (created in setUp)
    • Define methods to exclude from mocking via setMethodsExcept()
    • Return the configured mock object

Method Mocking Rules

  • Only mock methods that are expected to be called during test execution
  • Exception: Use expects(self::never()) when verifying a method should NOT be called
  • Mock each method called in the currently tested method, including SUT methods
  • Do not mock methods that are not involved in the test scenario

Example:

public function testClearSiteCache(): void
{
    $typo3Service = $this->getTypo3ServiceMockFor('clearSiteCache');
    
    $response = $this->getMockBuilder(ResponseInterface::class)
        ->disableOriginalConstructor()
        ->getMock();
        
    $typo3Service->expects(self::once())
        ->method('sendCommand')
        ->with('/otadmin/site/clear-cache', ['organization-id' => 1])
        ->willReturn($response);

    $typo3Service->clearSiteCache(1);
}

Visibility Issues

  • For protected methods: Create an intermediate testable class (see TestableTypo3Service example)
  • For private methods in SUT: Change visibility to protected to enable testing
  • Testable class pattern:
class TestableMyService extends MyService
{
    public function protectedMethodToTest(string $param): ResponseInterface
    {
        return parent::protectedMethodToTest($param);
    }
}

Test Naming Convention

  • Primary test covering most common usage: testMethod (where "Method" is the actual method name)
  • Variant tests append specific situation:
    • testMethodWhenNoInput
    • testMethodWithInvalidContext
    • testMethodWithSpecialCondition

Test Documentation

  • Use @see annotations to reference the tested method: ```php /**

    • @see MyService::clearSiteCache() */ public function testClearSiteCache(): void

      
      #### Test Structure Best Practices
      - **One test per execution branch/path**
      - **Use assertions** when the method should return a result
      - **Use expectations** when the method should call other methods
      - **Use `expects(self::never())`** when verifying a method should NOT be called
      - **Cover edge cases and error scenarios**
      - **Test method variants** (e.g., with/without optional parameters)
      
      #### Dependencies and Isolation
      - **All dependencies must be mocked** in `setUp` method
      - **Tests must be isolated and independent**
      - **Use `disableOriginalConstructor()`** for mock builders
      - **Mock return values** using `willReturn()` for expected behaviors
      
      #### Example Test Patterns
      
      **Simple method expectation:**
      ```php
      public function testCreateSite(): void
      {
      $typo3Service = $this->getTypo3ServiceMockFor('createSite');
          
      $response = $this->getMockBuilder(ResponseInterface::class)
      ->disableOriginalConstructor()->getMock();
              
      $typo3Service->expects(self::once())
      ->method('sendCommand')
      ->with('/otadmin/site/create', ['organization-id' => 1])
      ->willReturn($response);
      
      $typo3Service->createSite(1);
      }
      

Method with conditional parameters:

public function testSetSiteDomainWithRedirection(): void
{
    $typo3Service = $this->getTypo3ServiceMockFor('setSiteDomain');
    
    $response = $this->getMockBuilder(ResponseInterface::class)
        ->disableOriginalConstructor()->getMock();
        
    $typo3Service->expects(self::once())
        ->method('sendCommand')
        ->with('/otadmin/site/set-domain', [
            'organization-id' => 1, 
            'domain' => 'new-domain', 
            'redirect' => 1
        ])
        ->willReturn($response);

    $typo3Service->setSiteDomain(1, 'new-domain', true);
}

Running Tests

# Run all unit tests
docker exec ap2i php -d memory_limit=-1 vendor/phpunit/phpunit/phpunit --testsuite unit --configuration phpunit.xml.dist

# Run application tests (adjust testsuite as needed)
docker exec ap2i vendor/bin/phpunit --configuration phpunit.xml.dist

# Run specific test class
docker exec ap2i vendor/bin/phpunit tests/Unit/Service/MyDomain/MyServiceTest.php

# Run with coverage (if configured)
docker exec ap2i vendor/bin/phpunit --coverage-html coverage/

Code Quality

The project enforces strict code quality standards:

PHPStan (Static Analysis)

# Run analysis
docker exec ap2i vendor/bin/phpstan analyse

# Clear cache if needed
docker exec ap2i vendor/bin/phpstan clear-result-cache

PHP CS Fixer (Code Style)

# Check code style
docker exec ap2i php vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php

# Fix code style automatically
docker exec ap2i php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php

Messenger (Async Processing)

The application uses Symfony Messenger for async processing:

# Start consuming messages
docker exec ap2i php bin/console messenger:consume async

# Setup transports if needed
docker exec ap2i php bin/console messenger:setup-transports

Build Process

  • No special build process required beyond standard Symfony practices
  • Run docker exec ap2i composer install for dependencies
  • Run database migrations: docker exec ap2i php bin/console doctrine:migrations:migrate
  • Clear cache: docker exec ap2i php bin/console cache:clear

Code Style Guidelines

  • Follow PSR-12 coding standards (enforced by PHP CS Fixer)
  • Use strict typing where possible (PHP 8.2+ features encouraged)
  • Follow Symfony best practices for service organization
  • Organize code by business domain
  • Write comprehensive tests for all business logic
  • Use PHPStan level checks for type safety

Important Notes

  • This is a proprietary project (license: proprietary)
  • Uses custom OpenTalent packages and private GitLab repositories
  • Requires PHP 8.2 minimum
  • Database changes must be done via Doctrine migrations
  • Always run the full test suite before submitting changes