SOLID Prensipleri Nedir? Laravel ile Kod Örnekleri
SOLID prensiplerini Laravel projeleri üzerinden single responsibility, open closed, liskov, interface segregation ve dependency inversion örnekleriyle anlatıyorum.
SOLID prensipleri, nesne yönelimli programlamada kodun daha okunabilir, değiştirilebilir ve test edilebilir olması için kullanılan beş temel fikirdir. İlk duyunca biraz teorik gelebilir. Ama aslında SOLID günlük Laravel projelerinde sürekli karşımıza çıkar: controller şişmesin, ödeme sistemi kolay değişsin, mail yerine SMS ekleyince her yer kırılmasın, test yazarken veritabanı veya üçüncü parti servise mahkum kalmayalım diye vardır.
Bu yazıda SOLID nedir, neden hayatımızdadır ve Laravel’de nasıl uygulanır konusunu örneklerle anlatacağım. Türkçe’yi çok akademik yapmadan, gerçek projede işe yarayacak şekilde ilerleyelim.
SOLID Açılımı Nedir?
- S - Single Responsibility Principle: Bir sınıfın tek ana sorumluluğu olsun.
- O - Open/Closed Principle: Kod genişlemeye açık, değiştirmeye kapalı olsun.
- L - Liskov Substitution Principle: Alt sınıflar veya implementasyonlar beklenen davranışı bozmasın.
- I - Interface Segregation Principle: Sınıflar kullanmadığı metodlara zorlanmasın.
- D - Dependency Inversion Principle: Üst seviye kodlar somut sınıflara değil, soyutlamalara bağımlı olsun.
Neden SOLID Prensipleri Hayatımızdadır?
Çünkü yazılımda değişmeyen tek şey değişikliktir. Bugün sadece kredi kartı ile ödeme alırsın, yarın havale eklenir. Bugün sadece mail gönderirsin, yarın SMS ve WhatsApp bildirimi gelir. Bugün blog yazısı sadece veritabanından gelir, yarın cache veya harici API devreye girer. Kod baştan çok sıkı bağlı yazıldıysa her yeni istek küçük kriz gibi olur.
SOLID bize şunu söyler: Kodunu öyle ayır ki yeni özellik geldiğinde eski çalışan yeri gereksiz yere bozma. Laravel’de service container, contract/interface, Form Request, Notification, Job, Event, Policy ve Eloquent scope gibi parçalar SOLID’e uygun mimari kurmayı kolaylaştırır.
1. Single Responsibility Principle
Single Responsibility, “bir sınıfın değişmek için tek sebebi olsun” demektir. Laravel’de en sık ihlal edilen yer controller’lardır. Controller hem request validate eder, hem ödeme alır, hem fatura keser, hem mail gönderir, hem log yazar. Bir süre sonra controller çorba olur.
Kötü Örnek
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class OrderController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'customer_name' => ['required', 'string'],
'email' => ['required', 'email'],
'total' => ['required', 'numeric', 'min:1'],
]);
$order = Order::create($validated);
// Ödeme işlemi burada
// Mail gönderimi burada
// Loglama burada
// Fatura burada
Mail::raw('Siparişiniz alındı.', function ($message) use ($order) {
$message->to($order->email)->subject('Sipariş');
});
Log::info('Sipariş oluşturuldu', ['order_id' => $order->id]);
return redirect()->route('orders.show', $order);
}
}
Bu controller çok iş yapıyor. Yarın mail sistemi değişirse controller değişecek. Ödeme değişirse yine controller değişecek. Log formatı değişirse yine buraya dokunacağız.
Daha Temiz Laravel Örneği
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreOrderRequest;
use App\Services\OrderService;
class OrderController extends Controller
{
public function store(StoreOrderRequest $request, OrderService $orders)
{
$order = $orders->create($request->validated());
return redirect()->route('orders.show', $order);
}
}
Form Request validasyonla ilgilenir:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'customer_name' => ['required', 'string', 'max:120'],
'email' => ['required', 'email'],
'total' => ['required', 'numeric', 'min:1'],
];
}
}
Service iş akışını yönetir:
<?php
namespace App\Services;
use App\Models\Order;
use App\Notifications\OrderCreatedNotification;
use Illuminate\Support\Facades\Notification;
class OrderService
{
public function create(array $data): Order
{
$order = Order::create($data);
Notification::route('mail', $order->email)
->notify(new OrderCreatedNotification($order));
return $order;
}
}
Artık controller sadece HTTP akışında duruyor. Validasyon ayrı, iş kuralı ayrı, bildirim ayrı. SRP budur.
2. Open/Closed Principle
Open/Closed prensibi, kodun yeni davranışlara açık ama mevcut kodu sürekli değiştirmeye kapalı olmasını ister. Mesela ödeme sisteminde sadece kredi kartı varken sorun yok. Ama havale, iyzico, PayTR, Stripe gibi seçenekler geldikçe `if else` büyüyorsa tehlike başlar.
Kötü Örnek
class PaymentService
{
public function pay(string $type, int $amount): bool
{
if ($type === 'credit_card') {
// kredi kartı ödeme
return true;
}
if ($type === 'bank_transfer') {
// havale ödeme
return true;
}
if ($type === 'paypal') {
// paypal ödeme
return true;
}
throw new \InvalidArgumentException('Geçersiz ödeme tipi.');
}
}
Yeni ödeme tipi geldiğinde bu sınıfı açıp değiştirmek zorundayız. Zamanla risk artar.
Daha Temiz Örnek
Önce contract yazalım:
<?php
namespace App\Contracts;
interface PaymentGatewayInterface
{
public function pay(int $amount): bool;
}
Kredi kartı implementasyonu:
<?php
namespace App\Payments;
use App\Contracts\PaymentGatewayInterface;
class CreditCardGateway implements PaymentGatewayInterface
{
public function pay(int $amount): bool
{
// Kredi kartı sağlayıcısına istek atılır
return true;
}
}
Havale implementasyonu:
<?php
namespace App\Payments;
use App\Contracts\PaymentGatewayInterface;
class BankTransferGateway implements PaymentGatewayInterface
{
public function pay(int $amount): bool
{
// Havale talimatı oluşturulur
return true;
}
}
Service artık ödeme tipinin detayını bilmez:
<?php
namespace App\Services;
use App\Contracts\PaymentGatewayInterface;
class CheckoutService
{
public function __construct(
private readonly PaymentGatewayInterface $payment
) {
}
public function checkout(int $amount): bool
{
return $this->payment->pay($amount);
}
}
Laravel service container ile hangi gateway kullanılacağını bağlarız:
use App\Contracts\PaymentGatewayInterface;
use App\Payments\CreditCardGateway;
public function register(): void
{
$this->app->bind(PaymentGatewayInterface::class, CreditCardGateway::class);
}
Yeni ödeme yöntemi geldiğinde `CheckoutService` sınıfına dokunmak zorunda kalmayız. Yeni class ekler, binding veya factory tarafını değiştiririz. Open/Closed bu mantıktır.
3. Liskov Substitution Principle
Liskov prensibi biraz garip isimli ama fikri basit: Bir interface veya base class bekleyen kod, onun farklı implementasyonları geldiğinde bozulmamalı. Yani `PaymentGatewayInterface` bekleyen bir servis, kredi kartı gateway ile çalışıyorsa havale gateway ile de beklenmeyen şekilde patlamamalı.
Kötü Örnek
class BankTransferGateway implements PaymentGatewayInterface
{
public function pay(int $amount): bool
{
throw new \RuntimeException('Havale ile anında ödeme alınamaz.');
}
}
Bu sınıf interface’i uyguluyor ama beklenen davranışı bozuyor. `pay()` metodu ödeme sürecini başlatıp sonucu dönmeli. Eğer havale anında ödeme almıyorsa interface yanlış tasarlanmış olabilir.
Daha Sağlıklı Tasarım
interface PaymentGatewayInterface
{
public function startPayment(int $amount): PaymentResult;
}
Sonuç nesnesi:
class PaymentResult
{
public function __construct(
public readonly bool $successful,
public readonly string $message,
public readonly ?string $redirectUrl = null,
) {
}
}
Kredi kartı:
class CreditCardGateway implements PaymentGatewayInterface
{
public function startPayment(int $amount): PaymentResult
{
return new PaymentResult(true, 'Ödeme başarılı.');
}
}
Havale:
class BankTransferGateway implements PaymentGatewayInterface
{
public function startPayment(int $amount): PaymentResult
{
return new PaymentResult(false, 'Havale talimatı oluşturuldu.');
}
}
Burada iki sınıf da aynı sözleşmeye uyuyor. Biri başarılı ödeme döndürüyor, diğeri havale talimatı mesajı döndürüyor. Beklenen tip bozulmuyor.
4. Interface Segregation Principle
Interface Segregation, “sınıfları kullanmadığı metodları implement etmeye zorlama” der. Büyük ve her şeyi kapsayan interface’ler zamanla sıkıntı çıkarır.
Kötü Örnek
interface NotificationChannelInterface
{
public function sendMail(string $to, string $message): void;
public function sendSms(string $phone, string $message): void;
public function sendPush(int $userId, string $message): void;
}
Mail sınıfı SMS göndermek zorunda değil. SMS sınıfı push göndermek zorunda değil. Bu interface fazla geniş.
Daha Temiz Örnek
interface MailSenderInterface
{
public function sendMail(string $to, string $message): void;
}
interface SmsSenderInterface
{
public function sendSms(string $phone, string $message): void;
}
interface PushSenderInterface
{
public function sendPush(int $userId, string $message): void;
}
Her class sadece ihtiyacı olan interface’i uygular:
class MailgunSender implements MailSenderInterface
{
public function sendMail(string $to, string $message): void
{
// Mail gönder
}
}
class NetgsmSender implements SmsSenderInterface
{
public function sendSms(string $phone, string $message): void
{
// SMS gönder
}
}
Laravel’de bu özellikle bildirim, ödeme, dosya yükleme, raporlama gibi modüllerde işine yarar. Küçük interface, daha az zorunluluk ve daha net kod demektir.
5. Dependency Inversion Principle
Dependency Inversion, SOLID’in Laravel’de en çok hissedilen prensibidir. Üst seviye sınıflar somut sınıflara değil, interface gibi soyutlamalara bağlı olmalı. Laravel service container bu işi çok rahatlatır.
Kötü Örnek
class ReportService
{
public function export(array $data): string
{
$exporter = new CsvExporter();
return $exporter->export($data);
}
}
`ReportService` doğrudan `CsvExporter` sınıfına bağlı. Yarın Excel export gelirse bu sınıfı değiştirmek gerekecek.
Daha Temiz Örnek
interface ReportExporterInterface
{
public function export(array $data): string;
}
class CsvExporter implements ReportExporterInterface
{
public function export(array $data): string
{
return 'csv-file-path.csv';
}
}
class ExcelExporter implements ReportExporterInterface
{
public function export(array $data): string
{
return 'excel-file-path.xlsx';
}
}
Service soyutlamaya bağımlı olur:
class ReportService
{
public function __construct(
private readonly ReportExporterInterface $exporter
) {
}
public function export(array $data): string
{
return $this->exporter->export($data);
}
}
`AppServiceProvider` içinde binding:
use App\Contracts\ReportExporterInterface;
use App\Exporters\CsvExporter;
public function register(): void
{
$this->app->bind(ReportExporterInterface::class, CsvExporter::class);
}
Laravel container, `ReportService` oluştururken hangi exporter’ın verileceğini bilir. Bu dependency inversion’ın pratik karşılığıdır.
Laravel’de SOLID Nerelerde Karşımıza Çıkar?
- Form Request: Validasyonu controller’dan ayırır. SRP.
- Service sınıfları: İş kurallarını controller’dan ayırır. SRP.
- Interface ve binding: Somut sınıf yerine sözleşmeye bağımlı olmayı sağlar. DIP.
- Notification kanalları: Mail, SMS, Slack gibi davranışlar ayrılabilir. OCP ve ISP.
- Repository: Veri erişimini soyutlar. SRP ve DIP.
- Policy: Yetki kurallarını controller’dan ayırır. SRP.
- Jobs: Ağır işleri request akışından ayırır. SRP.
Mini Proje Örneği: Sipariş Oluşturma
SOLID’i tek bir örnekte toparlayalım. Sipariş oluşturacağız, ödeme alacağız ve bildirim göndereceğiz.
interface PaymentGatewayInterface
{
public function pay(int $amount): PaymentResult;
}
interface OrderNotifierInterface
{
public function notify(Order $order): void;
}
class OrderService
{
public function __construct(
private readonly PaymentGatewayInterface $payment,
private readonly OrderNotifierInterface $notifier,
) {
}
public function create(array $data): Order
{
$payment = $this->payment->pay($data['total']);
if (! $payment->successful) {
throw new \RuntimeException($payment->message);
}
$order = Order::create($data);
$this->notifier->notify($order);
return $order;
}
}
Bu sınıfın avantajı şu: Ödeme sağlayıcısını değiştirirken `OrderService` bozulmaz. Bildirim kanalını değiştirirken `OrderService` yine bozulmaz. Testte ödeme ve bildirim sınıflarını fake/mock yapabiliriz.
Test Örneği
class FakePaymentGateway implements PaymentGatewayInterface
{
public function pay(int $amount): PaymentResult
{
return new PaymentResult(true, 'Test ödeme başarılı.');
}
}
class FakeOrderNotifier implements OrderNotifierInterface
{
public bool $sent = false;
public function notify(Order $order): void
{
$this->sent = true;
}
}
public function test_order_can_be_created(): void
{
$payment = new FakePaymentGateway();
$notifier = new FakeOrderNotifier();
$service = new OrderService($payment, $notifier);
$order = $service->create([
'customer_name' => 'Batuhan',
'email' => 'test@example.com',
'total' => 1000,
]);
$this->assertNotNull($order);
$this->assertTrue($notifier->sent);
}
İşte SOLID’in gerçek faydası burada ortaya çıkar: Test yazarken gerçek banka API’sine, gerçek mail servisine veya karmaşık dış bağımlılıklara mecbur kalmayız.
Sık Yapılan Hatalar
Her şeye interface yazmak: Interface gerekli yerde güzeldir. Ama her küçük class için interface açmak projeyi kalabalıklaştırır.
Controller’ı tamamen boşaltmaya çalışmak: Controller hafif olmalı ama her satırı başka sınıfa kaçırmak da gereksiz olabilir.
Service içine her şeyi koymak: Controller şişmesin derken bu kez `OrderService` 1000 satır olursa yine aynı problem yaşanır.
SOLID’i ezber gibi uygulamak: Prensipler araçtır. Amaç daha iyi kod yazmaktır, pattern koleksiyonu yapmak değil.
Kısa Özet
SRP: Bir sınıf tek ana işi yapsın.
OCP: Yeni özellik eklerken eski kodu mümkün olduğunca bozmayalım.
LSP: Interface uygulayan sınıflar beklenen davranışı bozmasın.
ISP: Büyük interface yerine küçük ve amaca özel interface kullanalım.
DIP: Somut sınıfa değil, soyutlamaya bağımlı olalım.
Sonuç
SOLID prensipleri hayatımızdadır çünkü projeler büyür, müşteriler yeni istek getirir, ödeme sistemi değişir, bildirim kanalı artar, test ihtiyacı çıkar. Kod çok sıkı bağlıysa her değişiklik risk olur. SOLID, bu riski azaltmak için bize pratik bir düşünme şekli verir.
Laravel tarafında Form Request, Service, Policy, Notification, Job, Repository, Contract ve Service Container gibi yapılar SOLID’i uygulamayı kolaylaştırır. Ama unutma: SOLID iyi kod yazmak için pusuladır, her yere zorla uygulanacak bir reçete değildir. Projenin ihtiyacına göre sade başla, karmaşıklık arttıkça doğru soyutlamayı ekle.
Kaynak notu: Bu yazı Laravel 12 service container, contracts, dependency injection, Eloquent ve testing yaklaşımı temel alınarak hazırlanmıştır. SOLID prensipleri genel OOP prensipleridir; Laravel ise bunları pratikte uygulamak için çok uygun araçlar sağlar.