· Admin

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.