Dependency Injection
AvelPress includes a powerful dependency injection container that manages class dependencies and performs dependency injection. The container automatically resolves class dependencies by examining the constructor parameters using PHP's reflection capabilities.
Introduction to Dependency Injection
Dependency injection is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. Instead of a class creating its own dependencies, they are "injected" from the outside.
Benefits
- Loose Coupling: Classes depend on abstractions, not concrete implementations
- Testability: Easy to mock dependencies for unit testing
- Flexibility: Easy to swap implementations
- Maintainability: Changes to dependencies don't affect dependent classes
The Service Container
The AvelPress service container is the central registry for managing class dependencies and performing dependency injection.
Basic Container Usage
<?php
use AvelPress\AvelPress;
// Get the application container
$app = AvelPress::app();
// Bind a service to the container
$app->bind('payment.gateway', PaymentGateway::class);
// Resolve a service from the container
$gateway = $app->make('payment.gateway');
Binding Services
Simple Binding
The most basic way to register a service is using the bind
method:
<?php
// Bind a concrete class
$app->bind('mailer', EmailService::class);
// Bind an interface to an implementation
$app->bind(PaymentGatewayInterface::class, StripePaymentGateway::class);
// Bind with a closure
$app->bind('payment.processor', function () {
return new PaymentProcessor(
new StripeGateway(),
new EmailNotifier()
);
});
Singleton Binding
When you want the same instance to be returned every time:
<?php
// Bind as singleton
$app->singleton('database', function () {
return new DatabaseConnection([
'host' => 'localhost',
'database' => 'myapp',
'username' => 'user',
'password' => 'pass'
]);
});
// This will always return the same instance
$db1 = $app->make('database');
$db2 = $app->make('database');
// $db1 === $db2 (true)
Instance Binding
You can bind an existing instance to the container:
<?php
$logger = new FileLogger('/path/to/logs');
$app->instance('logger', $logger);
// Will always return the exact same instance
$retrievedLogger = $app->make('logger');
Automatic Resolution
The container can automatically resolve classes without explicit binding:
<?php
class EmailService
{
private $mailer;
private $logger;
public function __construct(MailerInterface $mailer, LoggerInterface $logger)
{
$this->mailer = $mailer;
$this->logger = $logger;
}
public function send($to, $subject, $message)
{
$this->logger->info("Sending email to: {$to}");
return $this->mailer->send($to, $subject, $message);
}
}
// The container will automatically resolve dependencies
$emailService = $app->make(EmailService::class);
Constructor Injection
The most common form of dependency injection in AvelPress is constructor injection:
<?php
namespace App\Services;
use App\Repositories\UserRepository;
use App\Services\EmailService;
class UserService
{
private $userRepository;
private $emailService;
public function __construct(UserRepository $userRepository, EmailService $emailService)
{
$this->userRepository = $userRepository;
$this->emailService = $emailService;
}
public function createUser(array $data): User
{
$user = $this->userRepository->create($data);
$this->emailService->send(
$user->email,
'Welcome!',
'Welcome to our application!'
);
return $user;
}
}
Method Injection
AvelPress also supports method-level dependency injection in controllers:
<?php
namespace App\Controllers;
use AvelPress\Routing\Controller;
use App\Services\UserService;
use App\Services\EmailService;
class UserController extends Controller
{
public function store($request, UserService $userService, EmailService $emailService)
{
// Dependencies are automatically injected
$user = $userService->createUser($request->all());
return response()->json($user);
}
public function sendWelcomeEmail($request, EmailService $emailService)
{
$user = User::find($request->get_param('id'));
$emailService->send(
$user->email,
'Welcome Back!',
'Thanks for returning to our app!'
);
return response()->json(['message' => 'Email sent successfully']);
}
}
Service Providers and Dependency Injection
Service providers are the perfect place to configure your container bindings:
<?php
namespace App\Providers;
use AvelPress\Support\ServiceProvider;
use App\Services\PaymentService;
use App\Services\EmailService;
use App\Contracts\PaymentGatewayInterface;
use App\Services\Gateways\StripeGateway;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind interfaces to implementations
$this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);
// Bind complex services
$this->app->singleton(PaymentService::class, function ($app) {
return new PaymentService(
$app->make(PaymentGatewayInterface::class),
$app->make(EmailService::class),
config('payment.default_currency', 'USD')
);
});
// Bind with configuration
$this->app->singleton('stripe.gateway', function () {
return new StripeGateway([
'api_key' => get_option('stripe_api_key'),
'webhook_secret' => get_option('stripe_webhook_secret'),
]);
});
}
}
WordPress Integration Examples
WordPress Service Integration
<?php
namespace App\Services;
class WordPressIntegrationService
{
private $optionsService;
private $metaService;
public function __construct(OptionsService $optionsService, MetaService $metaService)
{
$this->optionsService = $optionsService;
$this->metaService = $metaService;
}
public function getUserPreferences($userId): array
{
return [
'email_notifications' => $this->metaService->getUserMeta($userId, 'email_notifications'),
'theme_preference' => $this->optionsService->get('default_theme'),
'language' => get_user_locale($userId),
];
}
}
class OptionsService
{
public function get($key, $default = null)
{
return get_option($key, $default);
}
public function set($key, $value): bool
{
return update_option($key, $value);
}
}
class MetaService
{
public function getUserMeta($userId, $key, $single = true)
{
return get_user_meta($userId, $key, $single);
}
public function setUserMeta($userId, $key, $value): bool
{
return update_user_meta($userId, $key, $value) !== false;
}
public function getPostMeta($postId, $key, $single = true)
{
return get_post_meta($postId, $key, $single);
}
public function setPostMeta($postId, $key, $value): bool
{
return update_post_meta($postId, $key, $value) !== false;
}
}
Plugin Service Provider Example
<?php
namespace MyPlugin\Providers;
use AvelPress\Support\ServiceProvider;
use MyPlugin\Services\ProductService;
use MyPlugin\Services\OrderService;
use MyPlugin\Services\EmailService;
use MyPlugin\Repositories\ProductRepository;
use MyPlugin\Repositories\OrderRepository;
class PluginServiceProvider extends ServiceProvider
{
public function register(): void
{
// Repository bindings
$this->app->singleton(ProductRepository::class);
$this->app->singleton(OrderRepository::class);
// Service bindings with dependencies
$this->app->singleton(ProductService::class, function ($app) {
return new ProductService(
$app->make(ProductRepository::class),
$app->make(EmailService::class)
);
});
$this->app->singleton(OrderService::class, function ($app) {
return new OrderService(
$app->make(OrderRepository::class),
$app->make(ProductService::class),
$app->make(EmailService::class)
);
});
// WordPress-specific services
$this->app->singleton('wp.hooks', function () {
return new WordPressHookService();
});
}
public function boot(): void
{
// Register WordPress hooks using injected services
$orderService = $this->app->make(OrderService::class);
add_action('woocommerce_order_status_completed', [$orderService, 'handleOrderCompleted']);
add_action('wp_ajax_create_custom_order', [$orderService, 'createOrderViaAjax']);
}
}
Advanced Patterns
Factory Pattern with Dependency Injection
<?php
namespace App\Factories;
use App\Services\EmailService;
use App\Services\SMSService;
class NotificationFactory
{
private $emailService;
private $smsService;
public function __construct(EmailService $emailService, SMSService $smsService)
{
$this->emailService = $emailService;
$this->smsService = $smsService;
}
public function create($type)
{
switch ($type) {
case 'email':
return $this->emailService;
case 'sms':
return $this->smsService;
default:
throw new \InvalidArgumentException("Unknown notification type: {$type}");
}
}
}
// In a service provider
$this->app->singleton(NotificationFactory::class);
// Usage in a controller
public function sendNotification($request, NotificationFactory $factory)
{
$type = $request->get_param('type');
$notifier = $factory->create($type);
$notifier->send($request->get_param('message'));
}
Conditional Binding
<?php
// In a service provider
public function register(): void
{
// Bind different implementations based on environment
if (wp_get_environment_type() === 'production') {
$this->app->bind(LoggerInterface::class, ProductionLogger::class);
} else {
$this->app->bind(LoggerInterface::class, DebugLogger::class);
}
// Bind based on WordPress configuration
if (is_multisite()) {
$this->app->bind('cache.driver', MultisiteCacheDriver::class);
} else {
$this->app->bind('cache.driver', SingleSiteCacheDriver::class);
}
// Bind based on plugin availability
if (class_exists('WooCommerce')) {
$this->app->bind(ECommerceInterface::class, WooCommerceAdapter::class);
} elseif (class_exists('EDD')) {
$this->app->bind(ECommerceInterface::class, EDDAdapter::class);
} else {
$this->app->bind(ECommerceInterface::class, GenericECommerceAdapter::class);
}
}
Decorator Pattern with DI
<?php
namespace App\Services;
class CachedUserService implements UserServiceInterface
{
private $userService;
private $cache;
public function __construct(UserService $userService, CacheService $cache)
{
$this->userService = $userService;
$this->cache = $cache;
}
public function getUser($id): ?User
{
$cacheKey = "user.{$id}";
if ($cached = $this->cache->get($cacheKey)) {
return $cached;
}
$user = $this->userService->getUser($id);
if ($user) {
$this->cache->set($cacheKey, $user, 3600); // 1 hour
}
return $user;
}
}
// In service provider
$this->app->bind(UserServiceInterface::class, function ($app) {
$baseService = new UserService($app->make(UserRepository::class));
return new CachedUserService($baseService, $app->make(CacheService::class));
});
Testing with Dependency Injection
Dependency injection makes testing much easier:
<?php
use PHPUnit\Framework\TestCase;
class UserServiceTest extends TestCase
{
public function testCreateUserSendsWelcomeEmail()
{
// Create mocks
$userRepository = $this->createMock(UserRepository::class);
$emailService = $this->createMock(EmailService::class);
// Set up expectations
$userRepository->expects($this->once())
->method('create')
->willReturn(new User(['id' => 1, 'email' => 'test@example.com']));
$emailService->expects($this->once())
->method('send')
->with('test@example.com', 'Welcome!', $this->anything());
// Create service with injected mocks
$userService = new UserService($userRepository, $emailService);
// Test the method
$user = $userService->createUser(['email' => 'test@example.com']);
$this->assertInstanceOf(User::class, $user);
}
}
Best Practices
1. Use Interface Segregation
<?php
// Good: Small, focused interfaces
interface EmailSenderInterface
{
public function send($to, $subject, $message): bool;
}
interface EmailValidatorInterface
{
public function validate($email): bool;
}
// Bad: Large interface with many responsibilities
interface EmailInterface
{
public function send($to, $subject, $message): bool;
public function validate($email): bool;
public function queue($email): void;
public function process(): void;
public function retry($id): void;
}
2. Avoid Service Location Anti-pattern
<?php
// Bad: Service location
class UserController
{
public function store($request)
{
$userService = AvelPress::app(UserService::class); // Anti-pattern
return $userService->create($request->all());
}
}
// Good: Dependency injection
class UserController
{
private $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function store($request)
{
return $this->userService->create($request->all());
}
}
3. Use Type Hints
<?php
// Good: Clear type hints
public function __construct(
UserRepository $userRepository,
EmailService $emailService,
LoggerInterface $logger
) {
$this->userRepository = $userRepository;
$this->emailService = $emailService;
$this->logger = $logger;
}
// Bad: No type hints
public function __construct($userRepository, $emailService, $logger)
{
$this->userRepository = $userRepository;
$this->emailService = $emailService;
$this->logger = $logger;
}
The dependency injection container in AvelPress provides a robust foundation for building maintainable, testable, and flexible WordPress applications.