Files
thrillwiki_django_no_react/memory-bank/projects/django-to-symfony-conversion/05-conversion-strategy-summary.md

16 KiB

Django to Symfony Conversion Strategy Summary

Date: January 7, 2025
Analyst: Roo (Architect Mode)
Purpose: Comprehensive conversion strategy and challenge analysis
Status: Complete source analysis - Ready for Symfony implementation planning

Executive Summary

This document synthesizes the complete Django ThrillWiki analysis into a strategic conversion plan for Symfony. Based on detailed analysis of models, views, templates, and architecture, this document identifies key challenges, conversion strategies, and implementation priorities.

Conversion Complexity Assessment

High Complexity Areas (Significant Symfony Architecture Changes)

1. Generic Foreign Key System 🔴 CRITICAL

Challenge: Django's GenericForeignKey extensively used for Photos, Reviews, Locations

# Django Pattern
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')

Symfony Solutions:

  • Option A: Polymorphic inheritance mapping with discriminator
  • Option B: Interface-based approach with separate entities
  • Option C: Union types with service layer abstraction

Recommendation: Interface-based approach for maintainability

2. History Tracking System 🔴 CRITICAL

Challenge: @pghistory.track() provides automatic comprehensive history tracking

@pghistory.track()
class Park(TrackedModel):
    # Automatic history for all changes

Symfony Solutions:

  • Option A: Doctrine Extensions Loggable behavior
  • Option B: Custom event sourcing implementation
  • Option C: Third-party audit bundle (DataDog/Audit)

Recommendation: Doctrine Extensions + custom event sourcing for critical entities

3. PostGIS Geographic Integration 🟡 MODERATE

Challenge: PostGIS PointField and spatial queries

location = models.PointField(geography=True, null=True, blank=True)

Symfony Solutions:

  • Doctrine DBAL geographic types
  • CrEOF Spatial library for geographic operations
  • Custom repository methods for spatial queries

Medium Complexity Areas (Direct Mapping Possible)

4. Authentication & Authorization 🟡 MODERATE

Django Pattern:

@user_passes_test(lambda u: u.role in ['MODERATOR', 'ADMIN'])
def moderation_view(request):
    pass

Symfony Equivalent:

#[IsGranted('ROLE_MODERATOR')]
public function moderationView(): Response
{
    // Implementation
}

5. Form System 🟡 MODERATE

Django ModelForm → Symfony FormType

  • Direct field mapping possible
  • Validation rules transfer
  • HTMX integration maintained

6. URL Routing 🟢 LOW

Django URLs → Symfony Routes

  • Straightforward annotation conversion
  • Parameter types easily mapped
  • Route naming conventions align

Low Complexity Areas (Straightforward Migration)

7. Template System 🟢 LOW

Django Templates → Twig Templates

  • Syntax mostly compatible
  • Block structure identical
  • Template inheritance preserved

8. Static Asset Management 🟢 LOW

Django Static Files → Symfony Webpack Encore

  • Tailwind CSS configuration transfers
  • JavaScript bundling improved
  • Asset versioning enhanced

Conversion Strategy by Layer

1. Database Layer Strategy

Phase 1: Schema Preparation

-- Maintain existing PostgreSQL schema
-- Add Symfony-specific tables
CREATE TABLE doctrine_migration_versions (
    version VARCHAR(191) NOT NULL,
    executed_at DATETIME DEFAULT NULL,
    execution_time INT DEFAULT NULL
);

-- Add entity inheritance tables if using polymorphic approach
CREATE TABLE photo_type (
    id SERIAL PRIMARY KEY,
    type VARCHAR(50) NOT NULL
);

Phase 2: Data Migration Scripts

// Symfony Migration
public function up(Schema $schema): void
{
    // Migrate GenericForeignKey data to polymorphic structure
    $this->addSql('ALTER TABLE photo ADD discriminator VARCHAR(50)');
    $this->addSql('UPDATE photo SET discriminator = \'park\' WHERE content_type_id = ?', [$parkContentTypeId]);
}

2. Entity Layer Strategy

Core Entity Conversion Pattern

// Symfony Entity equivalent to Django Park model
#[ORM\Entity(repositoryClass: ParkRepository::class)]
#[ORM\HasLifecycleCallbacks]
#[Gedmo\Loggable]
class Park
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Gedmo\Versioned]
    private ?string $name = null;

    #[ORM\Column(length: 255, unique: true)]
    #[Gedmo\Slug(fields: ['name'])]
    private ?string $slug = null;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    #[Gedmo\Versioned]
    private ?string $description = null;

    #[ORM\Column(type: 'park_status', enumType: ParkStatus::class)]
    #[Gedmo\Versioned]
    private ParkStatus $status = ParkStatus::OPERATING;

    #[ORM\ManyToOne(targetEntity: Operator::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?Operator $operator = null;

    #[ORM\ManyToOne(targetEntity: PropertyOwner::class)]
    #[ORM\JoinColumn(nullable: true)]
    private ?PropertyOwner $propertyOwner = null;

    // Geographic data using CrEOF Spatial
    #[ORM\Column(type: 'point', nullable: true)]
    private ?Point $location = null;

    // Relationships using interface approach
    #[ORM\OneToMany(mappedBy: 'park', targetEntity: ParkPhoto::class)]
    private Collection $photos;

    #[ORM\OneToMany(mappedBy: 'park', targetEntity: ParkReview::class)]
    private Collection $reviews;
}

Generic Relationship Solution

// Interface approach for generic relationships
interface PhotoableInterface
{
    public function getId(): ?int;
    public function getPhotos(): Collection;
}

// Specific implementations
#[ORM\Entity]
class ParkPhoto
{
    #[ORM\ManyToOne(targetEntity: Park::class, inversedBy: 'photos')]
    private ?Park $park = null;

    #[ORM\Embedded(class: PhotoData::class)]
    private PhotoData $photoData;
}

#[ORM\Entity]  
class RidePhoto
{
    #[ORM\ManyToOne(targetEntity: Ride::class, inversedBy: 'photos')]
    private ?Ride $ride = null;

    #[ORM\Embedded(class: PhotoData::class)]
    private PhotoData $photoData;
}

// Embedded value object for shared photo data
#[ORM\Embeddable]
class PhotoData
{
    #[ORM\Column(length: 255)]
    private ?string $filename = null;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $caption = null;

    #[ORM\Column(type: Types::JSON)]
    private array $exifData = [];
}

3. Controller Layer Strategy

HTMX Integration Pattern

#[Route('/parks/{slug}', name: 'park_detail')]
public function detail(
    Request $request, 
    Park $park,
    ParkRepository $parkRepository
): Response {
    // Load related data
    $rides = $parkRepository->findRidesForPark($park);
    
    // HTMX partial response
    if ($request->headers->has('HX-Request')) {
        return $this->render('parks/partials/detail.html.twig', [
            'park' => $park,
            'rides' => $rides,
        ]);
    }
    
    // Full page response
    return $this->render('parks/detail.html.twig', [
        'park' => $park,
        'rides' => $rides,
    ]);
}

#[Route('/parks/{slug}/rides', name: 'park_rides_partial')]
public function ridesPartial(
    Request $request,
    Park $park,
    RideRepository $rideRepository
): Response {
    $filters = [
        'ride_type' => $request->query->get('ride_type'),
        'status' => $request->query->get('status'),
    ];
    
    $rides = $rideRepository->findByParkWithFilters($park, $filters);
    
    return $this->render('parks/partials/rides_section.html.twig', [
        'park' => $park,
        'rides' => $rides,
        'filters' => $filters,
    ]);
}

Authentication Integration

// Security configuration
security:
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: username
    
    firewalls:
        main:
            lazy: true
            provider: app_user_provider
            custom_authenticator: App\Security\LoginFormAuthenticator
            oauth:
                resource_owners:
                    google: "/login/google"
                    discord: "/login/discord"
    
    access_control:
        - { path: ^/moderation, roles: ROLE_MODERATOR }
        - { path: ^/admin, roles: ROLE_ADMIN }

// Voter system for complex permissions
class ParkEditVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        return $attribute === 'EDIT' && $subject instanceof Park;
    }
    
    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        
        if (!$user instanceof User) {
            return false;
        }
        
        // Allow moderators and admins to edit any park
        if (in_array('ROLE_MODERATOR', $user->getRoles())) {
            return true;
        }
        
        // Additional business logic
        return false;
    }
}

4. Service Layer Strategy

Repository Pattern Enhancement

class ParkRepository extends ServiceEntityRepository
{
    public function findByOperatorWithStats(Operator $operator): array
    {
        return $this->createQueryBuilder('p')
            ->select('p', 'COUNT(r.id) as rideCount')
            ->leftJoin('p.rides', 'r')
            ->where('p.operator = :operator')
            ->andWhere('p.status = :status')
            ->setParameter('operator', $operator)
            ->setParameter('status', ParkStatus::OPERATING)
            ->groupBy('p.id')
            ->orderBy('p.name', 'ASC')
            ->getQuery()
            ->getResult();
    }
    
    public function findNearby(Point $location, int $radiusKm = 50): array
    {
        return $this->createQueryBuilder('p')
            ->where('ST_DWithin(p.location, :point, :distance) = true')
            ->setParameter('point', $location)
            ->setParameter('distance', $radiusKm * 1000) // Convert to meters
            ->orderBy('ST_Distance(p.location, :point)')
            ->getQuery()
            ->getResult();
    }
}

Search Service Integration

class SearchService
{
    public function __construct(
        private ParkRepository $parkRepository,
        private RideRepository $rideRepository,
        private OperatorRepository $operatorRepository
    ) {}
    
    public function globalSearch(string $query, int $limit = 10): SearchResults
    {
        $parks = $this->parkRepository->searchByName($query, $limit);
        $rides = $this->rideRepository->searchByName($query, $limit);
        $operators = $this->operatorRepository->searchByName($query, $limit);
        
        return new SearchResults($parks, $rides, $operators);
    }
    
    public function getAutocompleteSuggestions(string $query): array
    {
        // Implement autocomplete logic
        return [
            'parks' => $this->parkRepository->getNameSuggestions($query, 5),
            'rides' => $this->rideRepository->getNameSuggestions($query, 5),
        ];
    }
}

Migration Timeline & Phases

Phase 1: Foundation (Weeks 1-2)

  • Set up Symfony 6.4 project structure
  • Configure PostgreSQL with PostGIS
  • Set up Doctrine with geographic extensions
  • Implement basic User entity and authentication
  • Configure Webpack Encore with Tailwind CSS

Phase 2: Core Entities (Weeks 3-4)

  • Create core entities (Park, Ride, Operator, etc.)
  • Implement entity relationships
  • Set up repository patterns
  • Configure history tracking system
  • Migrate core data from Django

Phase 3: Generic Relationships (Weeks 5-6)

  • Implement photo system with interface approach
  • Create review system
  • Set up location/geographic services
  • Migrate media files and metadata

Phase 4: Controllers & Views (Weeks 7-8)

  • Convert Django views to Symfony controllers
  • Implement HTMX integration patterns
  • Convert templates from Django to Twig
  • Set up routing and URL patterns

Phase 5: Advanced Features (Weeks 9-10)

  • Implement search functionality
  • Set up moderation workflow
  • Configure analytics and tracking
  • Implement form system with validation

Phase 6: Testing & Optimization (Weeks 11-12)

  • Migrate test suite to PHPUnit
  • Performance optimization and caching
  • Security audit and hardening
  • Documentation and deployment preparation

Critical Dependencies & Bundle Selection

Required Symfony Bundles

# composer.json equivalent packages
"require": {
    "symfony/framework-bundle": "^6.4",
    "symfony/security-bundle": "^6.4",
    "symfony/twig-bundle": "^6.4",
    "symfony/form": "^6.4",
    "symfony/validator": "^6.4",
    "symfony/mailer": "^6.4",
    "doctrine/orm": "^2.16",
    "doctrine/doctrine-bundle": "^2.11",
    "doctrine/migrations": "^3.7",
    "creof/doctrine2-spatial": "^1.6",
    "stof/doctrine-extensions-bundle": "^1.10",
    "knpuniversity/oauth2-client-bundle": "^2.15",
    "symfony/webpack-encore-bundle": "^2.1",
    "league/oauth2-google": "^4.0",
    "league/oauth2-discord": "^1.0"
}

Geographic Extensions

# Required system packages
apt-get install postgresql-contrib postgis
composer require creof/doctrine2-spatial

Risk Assessment & Mitigation

High Risk Areas

  1. Data Migration Integrity - Generic foreign key data migration

    • Mitigation: Comprehensive backup and incremental migration scripts
  2. History Data Preservation - Django pghistory → Symfony audit

    • Mitigation: Custom migration to preserve all historical data
  3. Geographic Query Performance - PostGIS spatial query optimization

    • Mitigation: Index analysis and query optimization testing

Medium Risk Areas

  1. HTMX Integration Compatibility - Ensuring seamless HTMX functionality

    • Mitigation: Progressive enhancement and fallback strategies
  2. File Upload System - Media file handling and storage

    • Mitigation: VichUploaderBundle with existing storage backend

Success Metrics

Technical Metrics

  • 100% Data Migration - All Django data successfully migrated
  • Feature Parity - All current Django features functional in Symfony
  • Performance Baseline - Response times equal or better than Django
  • Test Coverage - Maintain current test coverage levels

User Experience Metrics

  • UI/UX Consistency - No visual or functional regressions
  • HTMX Functionality - All dynamic interactions preserved
  • Mobile Responsiveness - Tailwind responsive design maintained
  • Accessibility - Current accessibility standards preserved

Conclusion

The Django ThrillWiki to Symfony conversion presents manageable complexity with clear conversion patterns for most components. The primary challenges center around Django's generic foreign key system and comprehensive history tracking, both of which have well-established Symfony solutions.

The interface-based approach for generic relationships and Doctrine Extensions for history tracking provide the most maintainable long-term solution while preserving all current functionality.

With proper planning and incremental migration phases, the conversion can be completed while maintaining data integrity and feature parity.

References


Status: COMPLETED - Django to Symfony conversion analysis complete
Next Phase: Symfony project initialization and entity design
Estimated Effort: 12 weeks with 2-3 developers
Risk Level: Medium - Well-defined conversion patterns with manageable complexity