Laravel'de IoC Container ve Contextual Binding
Dependency Injection duymuşsunuzdur ama Laravel'in Service Container'ının gerçek gücü Contextual Binding'de ortaya çıkıyor. Aynı interface'in farklı sınıflarda farklı implementasyonlarla çözümlenmesi — framework'ün en zarif özelliklerinden biri.
IoC (Inversion of Control) Nedir?
Bir sınıfın bağımlılıklarını doğrudan oluşturması yerine dışarıdan sağlanmasını ifade eder. Yani bağımlılıkları bir container yönetir.
Kötü örnek — sıkı bağlantı:
class UserService
{
private FileLogger $logger;
public function __construct()
{
$this->logger = new FileLogger(); // Doğrudan bağımlılık
}
}
İyi örnek — interface kullanımı:
interface LoggerInterface
{
public function log(string $message): void;
}
class FileLogger implements LoggerInterface
{
public function log(string $message): void
{
file_put_contents(storage_path('logs/app.log'), $message . PHP_EOL, FILE_APPEND);
}
}
class DatabaseLogger implements LoggerInterface
{
public function log(string $message): void
{
DB::table('logs')->insert(['message' => $message, 'created_at' => now()]);
}
}
Service Container Binding
Basit Binding
// AppServiceProvider
public function register(): void
{
$this->app->bind(LoggerInterface::class, FileLogger::class);
}
Artık LoggerInterface resolve edildiğinde FileLogger gelir:
class UserService
{
public function __construct(
private LoggerInterface $logger
) {}
public function createUser(array $data): User
{
$user = User::create($data);
$this->logger->log("Kullanıcı oluşturuldu: {$user->email}");
return $user;
}
}
Singleton Binding
Uygulama boyunca tek instance:
$this->app->singleton(LoggerInterface::class, FileLogger::class);
Instance Binding
Zaten oluşturulmuş bir nesneyi bind etme:
$logger = new FileLogger('/custom/path.log');
$this->app->instance(LoggerInterface::class, $logger);
Contextual Binding
İşin güzel kısmı burası. Farklı sınıflar için aynı interface'in farklı implementasyonlarını enjekte etme:
public function register(): void
{
// UserService → FileLogger alsın
$this->app->when(UserService::class)
->needs(LoggerInterface::class)
->give(FileLogger::class);
// AdminService → DatabaseLogger alsın
$this->app->when(AdminService::class)
->needs(LoggerInterface::class)
->give(DatabaseLogger::class);
// PaymentService → Slack'e log atsın
$this->app->when(PaymentService::class)
->needs(LoggerInterface::class)
->give(SlackLogger::class);
}
Hiçbir sınıf değişmedi — sadece container'a "kim neyi alacak" dedik. Sınıflar hâlâ LoggerInterface'a bağımlı.
Primitive Binding
Interface değil, basit değerler de contextual binding ile verilebilir:
$this->app->when(ReportService::class)
->needs('$reportPath')
->give(storage_path('reports'));
$this->app->when(MailService::class)
->needs('$fromAddress')
->give(config('mail.from.address'));
class ReportService
{
public function __construct(
private string $reportPath
) {}
}
Tagged Binding
Birden fazla implementasyonu gruplandırma:
$this->app->bind('file.logger', FileLogger::class);
$this->app->bind('db.logger', DatabaseLogger::class);
$this->app->bind('slack.logger', SlackLogger::class);
$this->app->tag(['file.logger', 'db.logger', 'slack.logger'], 'loggers');
// Tüm logger'ları alma
$this->app->when(LogAggregator::class)
->needs('$loggers')
->giveTagged('loggers');
class LogAggregator
{
public function __construct(
private array $loggers
) {}
public function log(string $message): void
{
foreach ($this->loggers as $logger) {
$logger->log($message);
}
}
}
Gerçek Dünya Örneği: Ödeme Sistemi
interface PaymentGateway
{
public function charge(float $amount): bool;
}
class StripeGateway implements PaymentGateway { /* ... */ }
class IyzicoGateway implements PaymentGateway { /* ... */ }
class PayTRGateway implements PaymentGateway { /* ... */ }
// Domestic siparişler → Iyzico
$this->app->when(DomesticOrderService::class)
->needs(PaymentGateway::class)
->give(IyzicoGateway::class);
// International siparişler → Stripe
$this->app->when(InternationalOrderService::class)
->needs(PaymentGateway::class)
->give(StripeGateway::class);
Test Kolaylığı
IoC'nin en büyük avantajı: test'te mock'lamak çok kolay:
public function test_user_creation_logs_message()
{
$mockLogger = Mockery::mock(LoggerInterface::class);
$mockLogger->shouldReceive('log')->once();
$this->app->instance(LoggerInterface::class, $mockLogger);
$service = app(UserService::class);
$service->createUser(['name' => 'Test', 'email' => '[email protected]']);
}
Sonuç
Laravel'in Service Container'ı sadece auto-wiring değil — contextual binding ile farklı sınıflara farklı implementasyonlar verebiliyorsunuz. Bu kod esnekliği, test edilebilirliği ve bakım kolaylığını ciddi şekilde artırır. Özellikle birden fazla entegrasyon (ödeme, loglama, bildirim) olan projelerde vazgeçilmez.