# 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 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: ```php 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:** ```php 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:** ```php 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 ```bash # 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) ```bash # 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) ```bash # 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: ```bash # 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