Skip to content

Add Doctrine Collection Generic Types

Priority: 🟠 P1 - HIGH (Quick Win) Status: Planning Related Analysis: phpstan-rector-analysis-overview.md

Problem Statement

69 PHPStan errors for missing generic type specifications on Doctrine Collections.

Current Issue

// ❌ Current - No generic type
public function getMarkets(): Collection
{
    return $this->markets;
}

// ✅ Desired - With generic type
/**
 * @return Collection<int, Market>
 */
public function getMarkets(): Collection
{
    return $this->markets;
}

Impact

  • Type Safety: PHPStan can't verify collection contents
  • IDE Support: No autocomplete for collection items
  • Maintainability: Unclear what collections contain
  • Refactoring: Harder to refactor safely

Guideline Violations

  • Type Safety: Missing type information
  • Documentation: Unclear data structures

Root Cause

Doctrine Collections are generic but PHPDocs don't specify element types. This is a common issue in Doctrine codebases that has accumulated over time.

Affected Entities

Based on PHPStan output, affected entities likely include:

  • AffiliateProgram - markets collection
  • Market - various relationships
  • CoffeeBean - multiple collections (flavors, regions, etc.)
  • Roaster - coffee beans, regions
  • Country - regions collection
  • Region - coffee beans, roasters
  • Species - varieties
  • Variety - coffee beans
  • ProcessingMethod - coffee beans
  • FlavorWheelNode - children nodes
  • All other entities with relationships

Proposed Solution

Use Rector to automatically add @return docblocks with generic types to all Doctrine Collection getters.

Rector Rule

CompleteReturnDocblockFromToManyRector - Already running and suggesting changes!

This rule:

  • Detects Doctrine @OneToMany and @ManyToMany relationships
  • Analyzes target entity from relationship mapping
  • Adds @return Collection<int, EntityType> docblock
  • Safe and reliable

Implementation Plan

Step 1: Backup Current State

git status  # Ensure clean working directory
git checkout -b type-safety/doctrine-collection-generics

Step 2: Run Rector (Dry Run First)

# See what would change
make rector --dry-run

# Or manually:
vendor/bin/rector process --dry-run

Step 3: Review Changes

  • Verify changes are adding docblocks only
  • Check that entity types are correct
  • Ensure no breaking changes

Step 4: Apply Changes

make rector

# Or manually:
vendor/bin/rector process

Step 5: Run PHPStan

make phpstan

Expected result: ~69 fewer errors!

Step 6: Manual Cleanup

Review and fix any edge cases:

  • Collections with custom keys (not int)
  • Collections with union types
  • Collections with interfaces

Step 7: Commit

git add .
git commit -m "Add generic types to Doctrine Collection getters

- Add @return Collection<int, Entity> docblocks to all entity getters
- Applied via Rector CompleteReturnDocblockFromToManyRector
- Fixes 69 PHPStan missingType.generics errors
- Improves IDE support and type safety"

Example Changes

Before

// Entity/Market.php
#[ORM\OneToMany(
    mappedBy: 'market',
    targetEntity: AffiliateProgram::class
)]
private Collection $affiliatePrograms;

public function getAffiliatePrograms(): Collection
{
    return $this->affiliatePrograms;
}

After

// Entity/Market.php
#[ORM\OneToMany(
    mappedBy: 'market',
    targetEntity: AffiliateProgram::class
)]
private Collection $affiliatePrograms;

/**
 * @return Collection<int, AffiliateProgram>
 */
public function getAffiliatePrograms(): Collection
{
    return $this->affiliatePrograms;
}

Edge Cases to Handle

Case 1: Collections with String Keys

/**
 * @return Collection<string, Country>
 */
public function getCountriesByCode(): Collection
{
    // Indexed by country code
}

Action: Manually update if Rector uses wrong key type

Case 2: Collections with Union Types

/**
 * @return Collection<int, CoffeeBean|DraftCoffeeBean>
 */
public function getBeans(): Collection
{
    // Could be either type
}

Action: Manually add if such cases exist

Case 3: Collections of Interfaces

/**
 * @return Collection<int, HasTimestamps>
 */
public function getTimestampedEntities(): Collection
{
    // Collection of interface implementations
}

Action: Rector should handle, verify correctness

Testing Strategy

Automated Testing

  1. Run existing test suite - should pass unchanged
  2. Run PHPStan - errors should decrease by ~69
  3. No functional changes - tests verify correctness

Manual Testing

  1. Check IDE autocomplete in collection iteration
  2. Verify PHPStan understands collection contents
  3. Test that no new errors introduced

Verification Checklist

  • [ ] All entity collection getters have docblocks
  • [ ] Generic types match relationship mappings
  • [ ] PHPStan errors reduced
  • [ ] No new PHPStan errors
  • [ ] All tests pass
  • [ ] IDE autocomplete works

Success Criteria

  • All Doctrine Collection return types have generic type hints
  • PHPStan missingType.generics errors reduced from 69 to <5
  • No functional changes (tests still pass)
  • Improved IDE autocomplete
  • Better type safety for refactoring

Risk Assessment

Very Low Risk:

  • Docblock-only changes
  • No runtime impact
  • Rector rule is battle-tested
  • Easy to revert if needed

Mitigation:

  • Dry run first
  • Review all changes
  • Run full test suite
  • Can revert commit if issues

Estimated Effort

Total: 2-3 hours

  • Backup and branch creation: 5 min
  • Run Rector dry run and review: 30 min
  • Apply Rector: 5 min
  • Manual cleanup of edge cases: 1 hour
  • Run PHPStan and verify: 15 min
  • Run test suite: 30 min
  • Commit and documentation: 15 min

Dependencies

None - can be done immediately

Follow-up Tasks

After completing this:

  1. Add generic types to custom collection classes (if any)
  2. Consider adding PHPStan rule to enforce generics on new code
  3. Update coding standards to require generic types
  4. Add to CI/CD to prevent regression

Notes

  • This is a quick win - high impact, low effort
  • Rector automates 95% of the work
  • Should be done BEFORE entity refactoring
  • Complements simplify-domain-entities.md plan
  • May reveal type mismatches in existing code (good thing!)
  • Helps with DTO refactoring (refactor-dto-parameter-lists.md)
  • Supports entity simplification (simplify-domain-entities.md)
  • Reduces overall PHPStan error count significantly