- 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.
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:
- Attribute Scanning:
UnifiedDiscoveryServicefinds all#[DefaultImplementation]attributes - Caching: Discovered attributes are cached along with other discovery results
- Processor Execution:
DefaultImplementationProcessorruns after discovery - 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, DiscoveryRegistry
✓ Explicit 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. Ifnull, 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