platform/doc/container_migration_guide.md
2024-11-25 20:33:45 -07:00

9.5 KiB

Container Migration Guide

Overview

This guide helps you migrate from the current Container implementation to the new Laravel-compatible version. It covers:

  1. Breaking changes
  2. New features
  3. Migration strategies
  4. Code examples
  5. Best practices

Breaking Changes

1. Binding Registration

Old Way

// Old implementation
container.bind<Service>(instance);
container.singleton<Service>(instance);

New Way

// New implementation
container.bind<Service>((c) => instance);
container.singleton<Service>((c) => instance);

// With contextual binding
container.when(UserController)
        .needs<Service>()
        .give((c) => SpecialService());

2. Service Resolution

Old Way

// Old implementation
var service = container.make<Service>();
var namedService = container.makeNamed<Service>('name');

New Way

// New implementation
var service = container.make<Service>();
var contextualService = container.make<Service>(context: UserController);
var taggedServices = container.taggedAs<Service>('tag');

3. Method Injection

Old Way

// Old implementation - manual parameter resolution
class UserService {
  void process(User user) {
    var logger = container.make<Logger>();
    var validator = container.make<Validator>();
    // Process user...
  }
}

New Way

// New implementation - automatic method injection
class UserService {
  void process(
    User user,
    Logger logger,  // Automatically injected
    Validator validator  // Automatically injected
  ) {
    // Process user...
  }
}

// Usage
container.call(userService, 'process', {'user': user});

New Features

1. Contextual Binding

// Register different implementations based on context
void setupBindings(Container container) {
  // Default storage
  container.bind<Storage>((c) => LocalStorage());
  
  // User uploads use cloud storage
  container.when(UserUploadController)
          .needs<Storage>()
          .give((c) => CloudStorage());
          
  // System files use local storage
  container.when(SystemFileController)
          .needs<Storage>()
          .give((c) => LocalStorage());
}

2. Tagged Bindings

// Register and tag related services
void setupReportServices(Container container) {
  // Register services
  container.bind<PerformanceReport>((c) => PerformanceReport());
  container.bind<FinancialReport>((c) => FinancialReport());
  container.bind<UserReport>((c) => UserReport());
  
  // Tag them for easy retrieval
  container.tag([
    PerformanceReport,
    FinancialReport,
    UserReport
  ], 'reports');
  
  // Additional categorization
  container.tag([PerformanceReport], 'metrics');
  container.tag([FinancialReport], 'financial');
}

// Usage
var reports = container.taggedAs<Report>('reports');
var metricReports = container.taggedAs<Report>('metrics');

3. Method Injection

class ReportGenerator {
  void generateReport(
    Report report,
    Logger logger,  // Automatically injected
    Formatter formatter,  // Automatically injected
    {required DateTime date}  // Manually provided
  ) {
    logger.info('Generating report...');
    var data = report.getData();
    var formatted = formatter.format(data);
    // Generate report...
  }
}

// Usage
container.call(
  generator,
  'generateReport',
  {'report': report, 'date': DateTime.now()}
);

Migration Strategies

1. Gradual Migration

// Step 1: Update bindings
class ServiceRegistry {
  void register(Container container) {
    // Old way (still works)
    container.bind<OldService>(OldService());
    
    // New way
    container.bind<NewService>((c) => NewService());
    
    // Add contextual bindings
    container.when(NewController)
            .needs<Service>()
            .give((c) => NewService());
  }
}

// Step 2: Update resolution
class ServiceConsumer {
  void process() {
    // Old way (still works)
    var oldService = container.make<OldService>();
    
    // New way
    var newService = container.make<NewService>();
    var contextual = container.make<Service>(
      context: NewController
    );
  }
}

// Step 3: Add method injection
class ServiceProcessor {
  // Old way
  void processOld(Data data) {
    var service = container.make<Service>();
    service.process(data);
  }
  
  // New way
  void processNew(
    Data data,
    Service service  // Injected automatically
  ) {
    service.process(data);
  }
}

2. Feature-by-Feature Migration

  1. Update Bindings First
// Update all bindings to new style
void registerBindings(Container container) {
  // Update simple bindings
  container.bind<Service>((c) => ServiceImpl());
  
  // Add contextual bindings
  container.when(Controller)
          .needs<Service>()
          .give((c) => SpecialService());
}
  1. Add Tagged Services
// Group related services
void registerServices(Container container) {
  // Register services
  container.bind<ServiceA>((c) => ServiceA());
  container.bind<ServiceB>((c) => ServiceB());
  
  // Add tags
  container.tag([ServiceA, ServiceB], 'services');
}
  1. Implement Method Injection
// Convert to method injection
class UserController {
  // Before
  void oldProcess(User user) {
    var validator = container.make<Validator>();
    var logger = container.make<Logger>();
    // Process...
  }
  
  // After
  void newProcess(
    User user,
    Validator validator,
    Logger logger
  ) {
    // Process...
  }
}

Testing During Migration

1. Verify Bindings

void main() {
  group('Container Migration Tests', () {
    late Container container;
    
    setUp(() {
      container = Container();
      registerBindings(container);
    });
    
    test('should support old-style bindings', () {
      var oldService = container.make<OldService>();
      expect(oldService, isNotNull);
    });
    
    test('should support new-style bindings', () {
      var newService = container.make<NewService>();
      expect(newService, isNotNull);
    });
    
    test('should resolve contextual bindings', () {
      var service = container.make<Service>(
        context: Controller
      );
      expect(service, isA<SpecialService>());
    });
  });
}

2. Verify Tagged Services

void main() {
  group('Tagged Services Tests', () {
    late Container container;
    
    setUp(() {
      container = Container();
      registerServices(container);
    });
    
    test('should resolve tagged services', () {
      var services = container.taggedAs<Service>('services');
      expect(services, hasLength(2));
      expect(services, contains(isA<ServiceA>()));
      expect(services, contains(isA<ServiceB>()));
    });
  });
}

3. Verify Method Injection

void main() {
  group('Method Injection Tests', () {
    late Container container;
    
    setUp(() {
      container = Container();
      registerServices(container);
    });
    
    test('should inject method dependencies', () {
      var controller = UserController();
      
      // Call with only required parameters
      container.call(
        controller,
        'newProcess',
        {'user': testUser}
      );
      
      // Verify injection worked
      verify(() => mockValidator.validate(any)).called(1);
      verify(() => mockLogger.log(any)).called(1);
    });
  });
}

Best Practices

  1. Update Bindings Consistently
// Good: Consistent new style
container.bind<Service>((c) => ServiceImpl());
container.singleton<Logger>((c) => FileLogger());

// Bad: Mixed styles
container.bind<Service>(ServiceImpl());  // Old style
container.singleton<Logger>((c) => FileLogger());  // New style
  1. Use Contextual Bindings Appropriately
// Good: Clear context and purpose
container.when(UserController)
        .needs<Storage>()
        .give((c) => UserStorage());

// Bad: Unclear or overly broad context
container.when(Object)
        .needs<Storage>()
        .give((c) => GenericStorage());
  1. Organize Tagged Services
// Good: Logical grouping
container.tag([
  PerformanceReport,
  SystemReport
], 'system-reports');

container.tag([
  UserReport,
  ActivityReport
], 'user-reports');

// Bad: Mixed concerns
container.tag([
  PerformanceReport,
  UserReport,
  Logger,
  Storage
], 'services');

Common Issues and Solutions

  1. Binding Resolution Errors
// Problem: Missing binding
var service = container.make<Service>();  // Throws error

// Solution: Register binding first
container.bind<Service>((c) => ServiceImpl());
var service = container.make<Service>();  // Works
  1. Contextual Binding Conflicts
// Problem: Multiple contexts
container.when(Controller)
        .needs<Service>()
        .give((c) => ServiceA());
        
container.when(Controller)  // Conflict!
        .needs<Service>()
        .give((c) => ServiceB());

// Solution: Use specific contexts
container.when(UserController)
        .needs<Service>()
        .give((c) => ServiceA());
        
container.when(AdminController)
        .needs<Service>()
        .give((c) => ServiceB());
  1. Method Injection Failures
// Problem: Unresolvable parameter
void process(
  CustomType param  // Not registered with container
) { }

// Solution: Register or provide parameter
container.bind<CustomType>((c) => CustomType());
// or
container.call(instance, 'process', {
  'param': customInstance
});

Next Steps

  1. Audit existing container usage
  2. Plan migration phases
  3. Update bindings
  4. Add new features
  5. Update tests
  6. Document changes

Would you like me to create detailed specifications for any of these next steps?