|
| 1 | +# Agent Instructions for API Platform Core Development |
| 2 | + |
| 3 | +This document provides guidelines for AI agents working on the API Platform core codebase. |
| 4 | + |
| 5 | +## Primary Responsibilities |
| 6 | + |
| 7 | +Unless explicitly asked otherwise: |
| 8 | + |
| 9 | +* **Focus on Test Writing:** Your main responsibility is to write functional tests for new features or bug fixes. |
| 10 | +* **No Bug Fixing:** Do not attempt to fix bugs; only write tests to expose them or verify fixes. |
| 11 | +* **Project Context:** API Platform is a PHP framework supporting both Symfony and Laravel. The user will specify which framework (defaults to Symfony). |
| 12 | +* **Fixture Handling:** Avoid altering existing fixtures to prevent unintended side effects on other tests. Create new entities/DTOs/models with unique names. Business logic is secondary; focus on framework testing. |
| 13 | +* **No Test Execution by Default:** Do not run tests unless explicitly asked (to save context, as tests produce verbose output). |
| 14 | + |
| 15 | +## Running Tests (When Asked) |
| 16 | + |
| 17 | +**⚠️ CRITICAL: Almost NEVER run the full test suite!** |
| 18 | + |
| 19 | +- Full Behat suite takes ~10 minutes (30+ minutes without `--format=progress`) |
| 20 | +- Full PHPUnit suite is also very slow |
| 21 | +- **Best Practice:** Identify failing tests from GitHub CI, then run those specific tests locally |
| 22 | + |
| 23 | +**Debugging Workflow:** |
| 24 | +1. Check GitHub CI to identify which specific test failed |
| 25 | +2. Run that exact test locally with verbose output for details |
| 26 | +3. Fix and verify only that test before pushing |
| 27 | + |
| 28 | +### Symfony Tests |
| 29 | + |
| 30 | +**Prerequisites:** |
| 31 | +- Clear cache when changing branches, dependencies, or `USE_SYMFONY_LISTENERS`: `rm -rf tests/Fixtures/app/var/cache/test` |
| 32 | +- Optionally warm cache: `tests/Fixtures/app/console cache:warmup` |
| 33 | +- For MongoDB tests: Set `APP_ENV=mongodb` and install MongoDB ODM: `composer require --dev doctrine/mongodb-odm-bundle` |
| 34 | +- MongoDB extension required: Ensure `mongodb` PHP extension is installed |
| 35 | + |
| 36 | +**PHPUnit (Main Suite):** |
| 37 | +```bash |
| 38 | +# ALWAYS use filtering - never run all tests unnecessarily! |
| 39 | +vendor/bin/phpunit --filter testMethodName |
| 40 | + |
| 41 | +# Filter by test class |
| 42 | +vendor/bin/phpunit --filter MyTestClass |
| 43 | + |
| 44 | +# Filter by path (recommended) |
| 45 | +vendor/bin/phpunit tests/Functional/SomeTest.php |
| 46 | + |
| 47 | +# Only if specifically needed (slow!) |
| 48 | +vendor/bin/phpunit |
| 49 | +``` |
| 50 | + |
| 51 | +**PHPUnit (Component-Specific):** |
| 52 | +```bash |
| 53 | +# Run tests for a specific component |
| 54 | +composer {component-name} test |
| 55 | + |
| 56 | +# Examples: |
| 57 | +composer doctrine-orm test |
| 58 | +composer graphql test |
| 59 | +composer metadata test |
| 60 | +``` |
| 61 | + |
| 62 | +**Behat (Functional Tests):** |
| 63 | +```bash |
| 64 | +# ⚠️ IMPORTANT: Always use --format=progress unless debugging! |
| 65 | +# Without it, tests take 3x longer (30min vs 10min) |
| 66 | + |
| 67 | +# Run specific scenario by line number (RECOMMENDED for debugging) |
| 68 | +vendor/bin/behat features/main/crud.feature:120 -vvv |
| 69 | + |
| 70 | +# Run specific feature file with progress (for CI-like run) |
| 71 | +vendor/bin/behat features/main/crud.feature --format=progress |
| 72 | + |
| 73 | +# Debug specific test (verbose, NO progress format) |
| 74 | +vendor/bin/behat features/main/crud.feature:120 -vvv |
| 75 | + |
| 76 | +# Filter by tags |
| 77 | +vendor/bin/behat --tags=@pagination --format=progress |
| 78 | + |
| 79 | +# With specific profile |
| 80 | +vendor/bin/behat --profile=default --format=progress |
| 81 | +vendor/bin/behat --profile=postgres --format=progress |
| 82 | +vendor/bin/behat --profile=default --tags '~@!mysql' --format=progress |
| 83 | + |
| 84 | +# Only run full suite if absolutely necessary (10+ minutes) |
| 85 | +vendor/bin/behat --format=progress |
| 86 | +``` |
| 87 | + |
| 88 | +**MongoDB Tests:** |
| 89 | +```bash |
| 90 | +# Set MongoDB environment |
| 91 | +export APP_ENV=mongodb |
| 92 | +export MONGODB_URL=mongodb://localhost:27017 |
| 93 | + |
| 94 | +# Install MongoDB ODM |
| 95 | +composer require --dev doctrine/mongodb-odm-bundle |
| 96 | + |
| 97 | +# Clear cache (always required when changing APP_ENV) |
| 98 | +rm -rf tests/Fixtures/app/var/cache/test |
| 99 | + |
| 100 | +# Run PHPUnit tests (exclude ORM tests) |
| 101 | +vendor/bin/phpunit --exclude-group=orm |
| 102 | + |
| 103 | +# Run Behat tests with MongoDB profile |
| 104 | +vendor/bin/behat --profile=mongodb-coverage --format=progress |
| 105 | +``` |
| 106 | + |
| 107 | +**Static Analysis:** |
| 108 | +```bash |
| 109 | +# Always run PHPStan to prevent trivial bugs |
| 110 | +# CRITICAL: PHPStan requires MongoDB extension AND MongoDB ODM bundle |
| 111 | +# Install MongoDB PHP extension first (e.g., pecl install mongodb) |
| 112 | +# Then install MongoDB ODM: |
| 113 | +composer require --dev doctrine/mongodb-odm-bundle |
| 114 | +vendor/bin/phpstan analyse --no-interaction --no-progress |
| 115 | + |
| 116 | +# PHPStan will fail without both: |
| 117 | +# - mongodb PHP extension (for analyzing Document fixtures) |
| 118 | +# - doctrine/mongodb-odm-bundle package (for ODM classes) |
| 119 | +``` |
| 120 | + |
| 121 | +**Testing Event Listeners (vs Default Event System):** |
| 122 | + |
| 123 | +API Platform has two modes for handling Symfony events: |
| 124 | + |
| 125 | +1. **Default Mode (Event System):** Uses Symfony's event system with event subscribers |
| 126 | +2. **Event Listeners Mode:** Uses traditional Symfony event listeners (enabled with `USE_SYMFONY_LISTENERS=1`) |
| 127 | + |
| 128 | +**When to test with event listeners:** |
| 129 | +- Set `USE_SYMFONY_LISTENERS=1` environment variable |
| 130 | +- Always clear cache after switching modes: `rm -rf tests/Fixtures/app/var/cache/test` |
| 131 | +- CI runs separate jobs for both modes to ensure compatibility |
| 132 | + |
| 133 | +**Special Note - Events vs Non-Events:** |
| 134 | +The event listeners mode (`USE_SYMFONY_LISTENERS=1`) changes how API Platform hooks into Symfony's lifecycle: |
| 135 | +- **Non-events (default):** Uses event subscribers for better performance and flexibility |
| 136 | +- **Events (listeners):** Uses traditional event listeners for backward compatibility |
| 137 | +- Both modes must be tested to ensure feature compatibility |
| 138 | +- When debugging event-related issues, test both modes |
| 139 | + |
| 140 | +### Laravel Tests |
| 141 | + |
| 142 | +**Setup:** |
| 143 | +```bash |
| 144 | +cd src/Laravel |
| 145 | +composer run-script build |
| 146 | +``` |
| 147 | + |
| 148 | +**PHPUnit:** |
| 149 | +```bash |
| 150 | +# Run Laravel tests |
| 151 | +cd src/Laravel |
| 152 | +vendor/bin/phpunit |
| 153 | + |
| 154 | +# Or via composer script |
| 155 | +composer run-script test |
| 156 | + |
| 157 | +# Filter tests |
| 158 | +vendor/bin/phpunit --filter testMethodName |
| 159 | +``` |
| 160 | + |
| 161 | +**Static Analysis:** |
| 162 | +```bash |
| 163 | +cd src/Laravel |
| 164 | +./vendor/bin/phpstan analyse --no-interaction --no-progress |
| 165 | +``` |
| 166 | + |
| 167 | +## Project Overview |
| 168 | + |
| 169 | +API Platform is a powerful, extensible, open-source PHP framework for building API-first projects. It leverages Symfony and Laravel to provide a robust foundation for creating REST and GraphQL APIs. |
| 170 | + |
| 171 | +* **Language:** PHP (with strict types: `declare(strict_types=1);`) |
| 172 | +* **Frameworks:** Symfony, Laravel |
| 173 | +* **Code Quality:** PSR-12 standard, enforced via `php-cs-fixer` and `phpstan` |
| 174 | +* **Testing:** PHPUnit (functional and unit tests), Behat (legacy functional tests - **do not add new Behat tests**) |
| 175 | + |
| 176 | +## Project Structure |
| 177 | + |
| 178 | +The codebase is organized into components supporting both Symfony and Laravel: |
| 179 | + |
| 180 | +**Core Components** (in `src/`): |
| 181 | +- `Doctrine/Common`, `Doctrine/Odm`, `Doctrine/Orm` - Doctrine integrations |
| 182 | +- `Documentation` - API documentation generation |
| 183 | +- `Elasticsearch` - Elasticsearch integration |
| 184 | +- `Graphql` - GraphQL support |
| 185 | +- `HttpCache` - HTTP caching |
| 186 | +- `Hydra` - Hydra JSON-LD vocabulary |
| 187 | +- `JsonApi` - JSON:API format support |
| 188 | +- `Hal` - HAL format support |
| 189 | +- `JsonSchema` - JSON Schema generation |
| 190 | +- `Jsonld` - JSON-LD support |
| 191 | +- `Laravel` - Laravel-specific implementation |
| 192 | +- `Metadata` - Resource metadata handling |
| 193 | +- `Openapi` - OpenAPI specification generation |
| 194 | +- `ParameterValidator` - Query parameter validation |
| 195 | +- `RamseyUuid` - UUID support |
| 196 | +- `Serializer` - Serialization layer |
| 197 | +- `State` - State management (providers/processors) |
| 198 | +- `Symfony` - Symfony-specific implementation |
| 199 | +- `Validator` - Validation support |
| 200 | + |
| 201 | +### Symfony Structure |
| 202 | + |
| 203 | +* **Fixtures:** |
| 204 | + * API Resources: `tests/Fixtures/TestBundle/ApiResource/` |
| 205 | + * Entities: `tests/Fixtures/TestBundle/Entity/` |
| 206 | + * Documents (MongoDB): `tests/Fixtures/TestBundle/Document/` (mirror of Entity/) |
| 207 | +* **Tests:** |
| 208 | + * Functional: `tests/Functional/` |
| 209 | + * Unit: Component-specific `src/{Component}/Tests/` |
| 210 | + * Behat features: `features/` (legacy - do not add new ones) |
| 211 | +* **Test App:** `tests/Fixtures/app/` (Symfony test application) |
| 212 | + |
| 213 | +**Fixture Examples:** |
| 214 | + |
| 215 | +1. **ApiResource with Static Provider (No Entity)** - `tests/Fixtures/TestBundle/ApiResource/Product.php` |
| 216 | + ```php |
| 217 | + #[Get(provider: [self::class, 'provide'])] |
| 218 | + class Product |
| 219 | + { |
| 220 | + #[ApiProperty(identifier: true)] |
| 221 | + public string $code; |
| 222 | + |
| 223 | + // Clever: Use a static method as provider to avoid entity persistence |
| 224 | + public static function provide() |
| 225 | + { |
| 226 | + $s = new self(); |
| 227 | + $s->code = 'test'; |
| 228 | + return $s; |
| 229 | + } |
| 230 | + } |
| 231 | + ``` |
| 232 | + |
| 233 | +2. **Entity with QueryParameter Filters** - `tests/Fixtures/TestBundle/Entity/Chicken.php` |
| 234 | + ```php |
| 235 | + #[ORM\Entity] |
| 236 | + #[GetCollection( |
| 237 | + parameters: [ |
| 238 | + 'chickenCoop' => new QueryParameter(filter: new IriFilter()), |
| 239 | + 'chickenCoopId' => new QueryParameter(filter: new ExactFilter(), property: 'chickenCoop'), |
| 240 | + 'name' => new QueryParameter(filter: new ExactFilter()), |
| 241 | + 'namePartial' => new QueryParameter( |
| 242 | + filter: new PartialSearchFilter(), |
| 243 | + property: 'name', |
| 244 | + ), |
| 245 | + 'autocomplete' => new QueryParameter( |
| 246 | + filter: new FreeTextQueryFilter(new OrFilter(new ExactFilter())), |
| 247 | + properties: ['name', 'ean'] |
| 248 | + ), |
| 249 | + 'q' => new QueryParameter( |
| 250 | + filter: new FreeTextQueryFilter(new PartialSearchFilter()), |
| 251 | + properties: ['name', 'ean'] |
| 252 | + ), |
| 253 | + ], |
| 254 | + )] |
| 255 | + class Chicken |
| 256 | + { |
| 257 | + #[ORM\Id] |
| 258 | + #[ORM\GeneratedValue] |
| 259 | + #[ORM\Column(type: 'integer')] |
| 260 | + private ?int $id = null; |
| 261 | + |
| 262 | + #[ORM\Column(type: 'string', length: 255)] |
| 263 | + private string $name; |
| 264 | + |
| 265 | + // ... getters/setters |
| 266 | + } |
| 267 | + ``` |
| 268 | + |
| 269 | +**Best Practices:** |
| 270 | +- Use static provider method pattern when you don't need database persistence |
| 271 | +- Use `Chicken.php` as reference for testing query parameters and filters on entities |
| 272 | +- Be smart: create new fixtures rather than modifying existing ones, adding a query parameter to chicken is perfectly fine as long as it doesn't alter the rest |
| 273 | + |
| 274 | +### Laravel Structure |
| 275 | + |
| 276 | +* **Base Directory:** `src/Laravel/` |
| 277 | +* **Tests:** `src/Laravel/Tests/` |
| 278 | + * Example tests: `EloquentTest.php`, `JsonLdTest.php` |
| 279 | +* **Fixtures/Models:** `src/Laravel/workbench/app/` |
| 280 | + * DTOs as ApiResource: `workbench/app/ApiResource/` |
| 281 | + * Eloquent Models: `workbench/app/Models/` (declared as resources with attributes) |
| 282 | +* **PHPUnit Binary:** `src/Laravel/vendor/bin/phpunit` |
| 283 | + |
| 284 | +## Code Style and Conventions |
| 285 | + |
| 286 | +**Critical:** Adherence to established code style is mandatory. |
| 287 | + |
| 288 | +### General Rules |
| 289 | + |
| 290 | +* **Standard:** PSR-12 (enforced via `.php-cs-fixer.dist.php`) |
| 291 | +* **Strict Types:** Always use `declare(strict_types=1);` at the top of PHP files |
| 292 | +* **Type Hints:** Provide type hints for all arguments and return types |
| 293 | +* **Imports:** Use `use` statements for all classes |
| 294 | + * Group by type: classes, functions, constants |
| 295 | + * Sort alphabetically within groups |
| 296 | +* **Naming Conventions:** |
| 297 | + * Classes: `PascalCase` |
| 298 | + * Methods: `camelCase` |
| 299 | + * Variables: `camelCase` |
| 300 | + * Constants: `UPPER_SNAKE_CASE` |
| 301 | +* **Visibility:** Always explicitly declare `public`, `protected`, or `private` |
| 302 | +* **Error Handling:** Use exceptions, not error codes |
| 303 | + |
| 304 | +### Key PHP-CS-Fixer Rules |
| 305 | + |
| 306 | +* **`@Symfony`, `@Symfony:risky`** - Comprehensive Symfony coding standards |
| 307 | +* **`@DoctrineAnnotation`** - Consistent Doctrine annotation formatting |
| 308 | +* **`strict_param`** - Strict type declarations for parameters |
| 309 | +* **`fully_qualified_strict_types`** - Fully qualified class names in type declarations |
| 310 | +* **`header_comment`** - Correct license header formatting |
| 311 | +* **`ordered_imports`** - Alphabetically sorted imports by type |
| 312 | +* **`no_superfluous_phpdoc_tags`** - Remove redundant PHPDoc tags |
| 313 | +* **`phpdoc_order`, `phpdoc_trim_consecutive_blank_line_separation`** - Consistent PHPDoc formatting |
| 314 | + |
| 315 | +### Validation Commands (Context Only) |
| 316 | + |
| 317 | +These commands run in CI - understand them but don't execute unless asked: |
| 318 | + |
| 319 | +```bash |
| 320 | +# Code style linting |
| 321 | +vendor/bin/php-cs-fixer fix --dry-run --diff |
| 322 | + |
| 323 | +# Static analysis |
| 324 | +vendor/bin/phpstan analyse |
| 325 | + |
| 326 | +# Component dependency check |
| 327 | +composer check-dependencies |
| 328 | + |
| 329 | +# Container linting (Symfony) |
| 330 | +tests/Fixtures/app/console lint:container |
| 331 | +``` |
| 332 | + |
| 333 | +## Contribution Guidelines |
| 334 | + |
| 335 | +### Branching Strategy |
| 336 | + |
| 337 | +* **New Features & Deprecations:** Target `main` branch |
| 338 | +* **Bug Fixes:** Target current stable branch (e.g., `4.x`) |
| 339 | +* **Legacy Code Removal:** Target `main` branch |
| 340 | + |
| 341 | +### Testing Requirements |
| 342 | + |
| 343 | +* **Always Add Tests:** PHPUnit functional tests are preferred |
| 344 | +* **No New Behat Tests:** Use PHPUnit instead (Behat is legacy) |
| 345 | +* **Tests Must Pass:** Ensure green CI before submitting |
| 346 | +* **Don't Modify Existing Tests:** Create new fixtures/resources instead |
| 347 | +* **Test Location:** |
| 348 | + * Symfony functional: `tests/Functional/` |
| 349 | + * Laravel functional: `src/Laravel/Tests/` |
| 350 | + * Component unit: `src/{Component}/Tests/` |
| 351 | + |
| 352 | +### Code Quality |
| 353 | + |
| 354 | +* **Never Break BC:** Backward compatibility is sacred (see https://symfony.com/bc) |
| 355 | +* **Update CHANGELOG:** Document changes in `CHANGELOG.md` |
| 356 | +* **Documentation PR:** Required for new features (`api-platform/docs` repository) |
| 357 | +* **Code Style:** Ensure `php-cs-fixer` and `phpstan` pass |
| 358 | + |
| 359 | +### Commit Messages |
| 360 | + |
| 361 | +Follow [Conventional Commits](https://www.conventionalcommits.org/): |
| 362 | + |
| 363 | +* **Types:** `fix`, `feat`, `docs`, `spec`, `test`, `perf`, `ci`, `chore` |
| 364 | +* **Format:** `type(scope): description` |
| 365 | +* **Examples:** |
| 366 | + * `fix(metadata): resource identifiers from properties` |
| 367 | + * `feat(validation): introduce a number constraint` |
| 368 | + * `test(graphql): add mutation validation tests` |
| 369 | +* **Scope:** Strongly recommended (component name) |
| 370 | +* **Note:** Only first commit needs conventional format (others are squashed) |
| 371 | + |
| 372 | +### PR Template |
| 373 | + |
| 374 | +```markdown |
| 375 | +| Q | A |
| 376 | +| ------------- | --- |
| 377 | +| Branch? | main for features / 4.x for bug fixes |
| 378 | +| Tickets | Closes #... |
| 379 | +| License | MIT |
| 380 | +| Doc PR | api-platform/docs#... (for features) |
| 381 | +``` |
| 382 | + |
| 383 | +**Checklist:** |
| 384 | +- [ ] Tests added and passing |
| 385 | +- [ ] No BC breaks |
| 386 | +- [ ] CHANGELOG.md updated |
| 387 | +- [ ] Documentation PR submitted (if feature) |
| 388 | +- [ ] Conventional commit format used |
| 389 | +- [ ] Code style passes (`php-cs-fixer`, `phpstan`) |
0 commit comments