Files
michaelschiemer/docs/DefaultImplementation-Attribute.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

13 KiB

DefaultImplementation Attribute

Automatic DI Container registration for interface implementations using attributes.

Overview

The #[DefaultImplementation] attribute enables automatic registration of classes as default implementations for interfaces in the DI container. This eliminates boilerplate container configuration while maintaining framework patterns (readonly, final, composition).

Features

  • Optional Interface Parameter: Specify interface explicitly or auto-detect
  • Auto-Detection: Automatically uses first implemented interface if not specified
  • Validation: Ensures class actually implements the specified/detected interface
  • Singleton Registration: All bindings are registered as singletons
  • Framework Integration: Fully integrated with Discovery system and caching
  • Type Safety: Full type safety with descriptive validation exceptions

Usage

Explicit Interface Specification

interface UserRepository
{
    public function findById(string $id): ?User;
}

#[DefaultImplementation(UserRepository::class)]
final readonly class DatabaseUserRepository implements UserRepository
{
    public function __construct(
        private readonly DatabaseConnection $db
    ) {}

    public function findById(string $id): ?User
    {
        // Implementation
    }
}

// Container automatically binds UserRepository::class => DatabaseUserRepository::class
$userRepo = $container->get(UserRepository::class); // DatabaseUserRepository instance

Auto-Detection (Single Interface)

interface LoggerInterface
{
    public function log(string $message): void;
}

#[DefaultImplementation]  // No explicit interface - auto-detects LoggerInterface
final readonly class FileLogger implements LoggerInterface
{
    public function log(string $message): void
    {
        // Implementation
    }
}

// Container automatically binds LoggerInterface::class => FileLogger::class
$logger = $container->get(LoggerInterface::class); // FileLogger instance

Auto-Detection (Multiple Interfaces)

When a class implements multiple interfaces, the first interface is used for auto-detection:

#[DefaultImplementation]  // Auto-detects LoggerInterface (first interface)
final readonly class AppLogger implements LoggerInterface, Stringable
{
    public function log(string $message): void
    {
        echo "[LOG] $message\n";
    }

    public function __toString(): string
    {
        return 'AppLogger';
    }
}

// Binds: LoggerInterface::class => AppLogger::class

Validation

The attribute validates interface implementation at application bootstrap:

Valid Usage

// ✓ Explicit interface that class implements
#[DefaultImplementation(UserRepository::class)]
final readonly class DatabaseUserRepository implements UserRepository {}

// ✓ Auto-detect with single interface
#[DefaultImplementation]
final readonly class FileLogger implements LoggerInterface {}

// ✓ Auto-detect with multiple interfaces (uses first)
#[DefaultImplementation]
final readonly class AppLogger implements LoggerInterface, Stringable {}

Invalid Usage (Throws Exceptions)

// ✗ Explicit interface NOT implemented
#[DefaultImplementation(LoggerInterface::class)]
final readonly class BadRepository implements UserRepository {}
// Throws: DefaultImplementationException
// "Class 'BadRepository' has #[DefaultImplementation] for interface 'LoggerInterface'
//  but does not implement that interface"

// ✗ Auto-detect with NO interfaces
#[DefaultImplementation]
final readonly class PlainClass {}
// Throws: DefaultImplementationException
// "Class 'PlainClass' has #[DefaultImplementation] without explicit interface but
//  implements no interfaces. Either specify an interface explicitly or ensure the
//  class implements at least one interface."

// ✗ Interface does not exist
#[DefaultImplementation(NonExistentInterface::class)]
final readonly class SomeClass implements ActualInterface {}
// Throws: DefaultImplementationException
// "Class 'SomeClass' has #[DefaultImplementation] for interface 'NonExistentInterface'
//  which does not exist"

How It Works

1. Discovery Phase

During application bootstrap, the Discovery system scans for #[DefaultImplementation] attributes:

// Automatic discovery via UnifiedDiscoveryService
$discoveryService = $container->get(UnifiedDiscoveryService::class);
$results = $discoveryService->discover();

2. Processing Phase

The DefaultImplementationProcessor processes discovered attributes:

// Automatically called in DiscoveryServiceBootstrapper
$processor = new DefaultImplementationProcessor($container);
$processor->process($results); // Registers all bindings

3. Container Registration

Each valid attribute creates a singleton binding:

// For each DefaultImplementation attribute:
$container->singleton($interface, $className);

4. Resolution

Classes are instantiated on-demand by the container:

$instance = $container->get(UserRepository::class);
// Returns: DatabaseUserRepository instance (singleton)

Integration Points

Discovery System

The attribute is automatically discovered via the framework's Discovery system:

  1. Attribute Scanning: UnifiedDiscoveryService finds all #[DefaultImplementation] attributes
  2. Caching: Discovered attributes are cached along with other discovery results
  3. Processor Execution: DefaultImplementationProcessor runs after discovery
  4. Before Initializers: Processed before #[Initializer] methods to ensure dependencies are available

Bootstrap Flow

AppBootstrapper
  └─> ContainerBootstrapper
       └─> DiscoveryServiceBootstrapper
            ├─> UnifiedDiscoveryService::discover()
            ├─> DefaultImplementationProcessor::process()  ← Registers bindings
            └─> InitializerProcessor::processInitializers() ← Can depend on bindings

Framework Compliance

The implementation follows all Custom PHP Framework principles:

No Inheritance: DefaultImplementationProcessor is final and uses composition ✓ Readonly Classes: Attribute and processor are readonly ✓ Value Objects: Uses ClassName, DiscoveredAttribute, DiscoveryRegistryExplicit DI: Constructor injection throughout ✓ Attribute-Driven: Convention over configuration ✓ Discovery Integration: Automatic registration via framework's attribute system

Testing

Comprehensive tests verify all scenarios:

# Run test suite
php tests/debug/test-default-implementation.php

Test Coverage:

  • ✓ Explicit interface specification
  • ✓ Auto-detect single interface
  • ✓ Auto-detect multiple interfaces (uses first)
  • ✓ Validation: interface not implemented
  • ✓ Validation: no interfaces implemented
  • ✓ Validation: interface does not exist
  • ✓ Multiple bindings in one registry
  • ✓ Singleton registration verification

File Structure

src/Framework/DI/
├── Attributes/
│   └── DefaultImplementation.php          # Attribute class
├── DefaultImplementationProcessor.php     # Registration processor
└── Exceptions/
    └── DefaultImplementationException.php # Validation exceptions

src/Framework/Discovery/
└── DiscoveryServiceBootstrapper.php       # Integration point (modified)

tests/debug/
└── test-default-implementation.php        # Comprehensive test suite

API Reference

#[DefaultImplementation]

Attribute

#[Attribute(Attribute::TARGET_CLASS)]
final readonly class DefaultImplementation
{
    public function __construct(
        public ?string $interface = null
    ) {}
}

Parameters:

  • $interface (optional): Interface class-string. If null, auto-detect first interface.

Target: CLASS

DefaultImplementationProcessor

Processor Class

final readonly class DefaultImplementationProcessor
{
    public function __construct(
        private readonly Container $container
    ) {}

    public function process(DiscoveryRegistry $registry): int;
}

Methods:

  • process(DiscoveryRegistry $registry): int - Processes attributes and registers bindings. Returns count of registered bindings.

DefaultImplementationException

Exception Class

final class DefaultImplementationException extends RuntimeException
{
    public static function doesNotImplementInterface(
        string $className,
        string $interface
    ): self;

    public static function noInterfacesImplemented(
        string $className
    ): self;

    public static function interfaceDoesNotExist(
        string $className,
        string $interface
    ): self;
}

Best Practices

When to Use

Service Implementations: Default implementations for repository, service interfaces ✓ Infrastructure Adapters: Database, cache, HTTP client implementations ✓ Domain Services: Business logic services implementing domain interfaces ✓ Framework Extensions: Custom implementations of framework interfaces

When NOT to Use

Multiple Implementations: When you need multiple implementations of same interface (use explicit binding) ✗ Conditional Logic: When implementation choice depends on runtime configuration (use Initializers) ✗ Non-Singleton Scopes: When you need per-request or transient instances (use factories)

Combining with Initializers

For complex setup, combine with #[Initializer]:

interface CacheService {}

#[DefaultImplementation(CacheService::class)]
final readonly class RedisCacheService implements CacheService
{
    public function __construct(
        private readonly RedisConnection $redis
    ) {}
}

final readonly class CacheServiceInitializer
{
    #[Initializer]
    public function initializeRedisConnection(): RedisConnection
    {
        // Complex Redis connection setup
        return new RedisConnection(/* config */);
    }
}

// DefaultImplementation registered first, then Initializer runs
// RedisCacheService can depend on RedisConnection from Initializer

Performance

  • Discovery Overhead: Negligible - scanned once during bootstrap and cached
  • Resolution Overhead: Zero - standard container resolution (singleton pattern)
  • Memory Impact: Minimal - only stores bindings in container registry
  • Cache Integration: Full cache support - discovered once, cached for subsequent requests

Troubleshooting

"Class does not implement interface"

Problem: Class marked with explicit interface but doesn't actually implement it.

Solution: Either implement the interface or remove the explicit parameter to auto-detect.

"Must implement at least one interface"

Problem: Class marked with #[DefaultImplementation] but implements no interfaces.

Solution: Either implement an interface or remove the attribute.

"Interface does not exist"

Problem: Explicit interface parameter references non-existent interface.

Solution: Fix the interface class-string or ensure the interface is loaded.

"Multiple implementations conflict"

Problem: Multiple classes have #[DefaultImplementation] for same interface.

Behavior: Last processed binding wins (discovery order dependent).

Solution: Remove attribute from all but one implementation, or use explicit container binding.

Migration Guide

From Manual Container Binding

Before:

// In Initializer or bootstrap
$container->singleton(UserRepository::class, DatabaseUserRepository::class);
$container->singleton(LoggerInterface::class, FileLogger::class);

After:

// Add attributes to implementation classes
#[DefaultImplementation(UserRepository::class)]
final readonly class DatabaseUserRepository implements UserRepository {}

#[DefaultImplementation(LoggerInterface::class)]
final readonly class FileLogger implements LoggerInterface {}

// Bindings happen automatically - remove manual registration

From Service Provider Pattern

Before:

final readonly class ServiceProvider
{
    public function register(Container $container): void
    {
        $container->singleton(UserRepository::class, DatabaseUserRepository::class);
        $container->singleton(LoggerInterface::class, FileLogger::class);
    }
}

After:

// Add attributes to implementation classes
#[DefaultImplementation(UserRepository::class)]
final readonly class DatabaseUserRepository implements UserRepository {}

#[DefaultImplementation]  // Auto-detect
final readonly class FileLogger implements LoggerInterface {}

// Remove ServiceProvider - attribute handles registration

Future Enhancements

Potential future improvements:

  • Conditional Registration: #[DefaultImplementation(when: 'production')]
  • Priority System: #[DefaultImplementation(priority: 10)] for multiple implementations
  • Scope Control: #[DefaultImplementation(scope: 'transient')] for non-singleton instances
  • Tag System: #[DefaultImplementation(tags: ['database', 'read-only'])] for grouped registration