Laravel'de servis sağlayıcı ve container'ı doğru kullanmak
Laravel'in bağımlılık çözüm mekanizmasını gerçekten anlamak: servis sağlayıcı ve IoC container'ı doğru kullanmanın pratik rehberi.
Laravel ile uzun süre çalışan bir geliştirici olarak itiraf etmeliyim: IoC container’ı yıllarca “oraya bağlama yap, oraya resolve et” düzeyinde kullandım. Ne yaptığını biliyordum ama neden böyle çalıştığını, sınırlarının nerede olduğunu ve service provider’ları gerçekten nasıl kullanmak gerektiğini tam olarak içselleştirmedim. Bu yazı, o boşluğu doldurmaya çalışırken öğrendiklerimin bir özeti.
Container nedir, ne değildir?
Laravel’in container’ı, bir sınıfın bağımlılıklarını otomatik olarak çözümleyen bir yapıdır. Bir sınıfı app()->make(FooService::class) ile çağırdığınızda, Laravel o sınıfın constructor’ına baktığını ve tip belirtilen her parametreyi kendi container’ından çözümlemeye çalışır.
<?php
// PaymentService, bir Gateway arayüzüne bağımlı
class PaymentService
{
public function __construct(private GatewayInterface $gateway)
{
}
}
Container bu bağımlılığı otomatik çözebilmek için GatewayInterface’in neyle eşleştiğini bilmesi gerekiyor. İşte o bildirim, servis sağlayıcıda yapılıyor.
Servis sağlayıcının tek görevi var
Servis sağlayıcı, container’a binding kaydetmektir. Başka hiçbir şey yapmamalı. Bunu fark ettiğimde birçok şey netleşti. register() metodu yalnızca bağlama için, boot() metodu ise uygulamanın tüm sağlayıcıları yüklenip kayıtlandıktan sonra çalışması gereken başlangıç kodları için ayrılmış.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\GatewayInterface;
use App\Services\IyzicoGateway;
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(GatewayInterface::class, function ($app) {
return new IyzicoGateway(
config('payment.api_key'),
config('payment.secret_key')
);
});
}
public function boot(): void
{
// Burada route yükleyebilir, view bileşen kaydı yapabilirsiniz
// Başka bir sağlayıcının register() metoduna bağımlı işler varsa buraya
}
}
register() içinde config(), auth() gibi facade’lere güvenmemek gerekiyor — o sırada tüm sağlayıcılar henüz yüklenmemiş olabilir. Bu kısıt başlangıçta garip geldi; ama mantığını anlayınca “kayıt” ve “başlatma” ayrımının ne kadar temiz olduğunu gördüm.
Singleton mu, bind mi?
Container’a bağlama yaparken iki temel seçenek var:
bind(): hermake()çağrısında yeni bir instance üretilir.singleton(): ilk çağrıda üretilir, sonraki çağrılarda aynı örnek döner.
<?php
// Her istekte yeni bir logger örneği istiyorsak:
$this->app->bind(LoggerInterface::class, FileLogger::class);
// Tek bir cache nesnesi yeterliyse:
$this->app->singleton(CacheInterface::class, function ($app) {
return new RedisCache($app['config']['cache.prefix']);
});
Bu ayrımı gözden kaçırmak sinsi hatalara yol açabiliyor. Özellikle stateful bir sınıfı singleton olarak kaydettiğinizde, bir istekten gelen durum bir sonrakine sızabilir. Tam tersine, her istekte temiz başlaması gereken bir sınıfı singleton yaparsanız tutarsız davranışlar görürsünüz.
Gerçekten ne zaman özel sağlayıcı yazmalı?
Laravel uygulamalarında görülen yaygın bir hata: her küçük şey için ayrı bir servis sağlayıcı oluşturmak. AppServiceProvider yeterince temiz tutulursa, birçok proje için tek sağlayıcı yeterlidir.
Özel bir sağlayıcı yazmak mantıklı olduğunda:
- Bir paketi veya dış servisi entegre ediyorsunuz ve bu entegrasyon kendi konfigürasyonunu, rotalarını, view’larını taşıyor.
- Bağlamaların sayısı
AppServiceProvider’ı okumayı zorlaştıracak kadar çoğaldı. - Bir modülün tüm servislerini, konfigürasyonlarını ve çevresini bir arada tutmak istiyorsunuz.
Aksi hâlde “her servis için bir sağlayıcı” yaklaşımı, gereksiz bir soyutlama katmanı ekliyor.
Trade-off’ları açıkça söylemek
Container’ın sağladığı esnekliğin bir bedeli var: IDE’nin “nereye gidiyor?” sorusunu yanıtlaması zorlaşıyor. Bir arayüzü implemente eden sınıf, container kaydına bakılmadan bulunamıyor. PHPStan ve Psalm gibi statik analiz araçları bu binding’leri kısmen anlıyor ama tam otomatik değil.
Benim yaklaşımım: arayüz-implementasyon bağlamalarını önemli bağımlılıklar için kullanmak, basit sınıflar için container’ı aradan çıkarmak. Her şeyi container’a yönlendirmek, esneklik adına okunabilirlikten taviz vermek olabilir.
Laravel’in container’ını anlamak, framework’ü “sihirli bir kutu” olarak görmekten çıkıp gerçek bir araç olarak kullanmaya geçiş sayılabilir. Bu geçiş, en azından benim için zaman aldı.