Varsayılan Laravel Yapısının Sorunu
Laravel'in varsayılan yapısı küçük ve orta ölçekli projeler için mükemmeldir. Ancak uygulamanız büyüdükçe, app/Models, app/Http/Controllers ve app/Services klasörleri birbiriyle ilgisiz kodların toplandığı yerlere dönüşür. Bir User modeli aynı anda kimlik doğrulama, faturalandırma, bildirim tercihleri ve takım yönetimini üstlenebilir.
Domain-Driven Design (DDD), kodu teknik katmanlar yerine iş kavramları etrafında düzenlememiz için bir terminoloji ve desen seti sunar.
Yeniden Yapılandırma: Katmanlardan Alan Adlarına
Teknik kaygılara göre değil, iş alanına göre düzenleyin:
app/
├── Domain/
│ ├── Billing/
│ │ ├── Models/
│ │ │ ├── Invoice.php
│ │ │ └── Subscription.php
│ │ ├── Actions/
│ │ │ ├── CreateInvoice.php
│ │ │ └── ProcessPayment.php
│ │ ├── Events/
│ │ │ └── PaymentReceived.php
│ │ ├── ValueObjects/
│ │ │ └── Money.php
│ │ └── Exceptions/
│ │ └── PaymentFailedException.php
│ ├── Identity/
│ │ ├── Models/
│ │ │ ├── User.php
│ │ │ └── Team.php
│ │ └── Actions/
│ │ ├── RegisterUser.php
│ │ └── InviteTeamMember.php
│ └── Catalog/
│ ├── Models/
│ └── Actions/
├── App/
│ ├── Http/Controllers/ ← thin controllers
│ ├── Console/Commands/
│ └── Providers/
Value Object'ler: İş Kurallarını Kapsülleme
Value object, kimliği olmayan ve tamamen nitelikleriyle tanımlanan değişmez (immutable) bir nesnedir. Money, Email, Address klasik örneklerdir.
class Money
{
public function __construct(
private readonly int $amount,
private readonly string $currency = 'USD'
) {
if ($amount < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new CurrencyMismatchException();
}
return new self($this->amount + $other->amount, $this->currency);
}
public function multiply(int $factor): self
{
return new self($this->amount * $factor, $this->currency);
}
public function formatted(): string
{
return number_format($this->amount / 100, 2) . ' ' . $this->currency;
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
Artık ham tam sayıları oradan oraya geçirip herkesin "bu cent mi yoksa dolar mı?" diye hatırlamasını ummak yerine, kendini belgeleyen ve kendini doğrulayan bir türünüz var.
Action'lar: Tek Amaçlı İş Operasyonları
Action'lar (ayrıca "use case" veya "command" olarak da bilinir) tek bir iş operasyonunu kapsüller. Alan mantığınıza giriş noktasıdırlar.
class CreateInvoice
{
public function __construct(
private readonly InvoiceRepository $invoices,
private readonly TaxCalculator $taxCalculator,
) {}
public function execute(CreateInvoiceData $data): Invoice
{
$subtotal = $data->lineItems->sum(
fn (LineItem $item) => $item->total()->amount
);
$tax = $this->taxCalculator->calculate(
new Money($subtotal),
$data->taxRegion
);
$invoice = $this->invoices->create([
'customer_id' => $data->customerId,
'subtotal' => $subtotal,
'tax' => $tax->amount,
'total' => $subtotal + $tax->amount,
'due_date' => $data->dueDate,
'status' => InvoiceStatus::Draft,
]);
event(new InvoiceCreated($invoice));
return $invoice;
}
}
Domain Event'ler: Yan Etkileri Ayrıştırma
Alan mantığınızda önemli bir şey olduğunda, bir event tetikleyin. Bu, çekirdek mantığınızı temiz tutar ve sistemin diğer bölümlerinin bağımsız olarak tepki vermesini sağlar.
// The event — a simple data carrier
class PaymentReceived
{
public function __construct(
public readonly int $invoiceId,
public readonly Money $amount,
public readonly string $paymentMethod,
public readonly Carbon $paidAt,
) {}
}
// Listeners handle side effects
class SendPaymentConfirmation
{
public function handle(PaymentReceived $event): void
{
$invoice = Invoice::findOrFail($event->invoiceId);
Mail::to($invoice->customer->email)->send(
new PaymentConfirmationMail($invoice, $event->amount)
);
}
}
class UpdateAccountBalance
{
public function handle(PaymentReceived $event): void
{
// Update the customer's balance
}
}
class NotifyAccountingTeam
{
public function handle(PaymentReceived $event): void
{
// Send Slack notification
}
}
Data Transfer Object'ler
DTO'lar, davranış içermeden katmanlar arasında veri taşır. Fonksiyon imzalarınızı açık ve yeniden düzenlemeye uygun hale getirir.
class CreateInvoiceData
{
public function __construct(
public readonly int $customerId,
public readonly Collection $lineItems,
public readonly string $taxRegion,
public readonly Carbon $dueDate,
) {}
public static function fromRequest(Request $request): self
{
return new self(
customerId: $request->integer('customer_id'),
lineItems: collect($request->input('line_items'))->map(
fn ($item) => new LineItem($item['description'], $item['quantity'], $item['unit_price'])
),
taxRegion: $request->string('tax_region'),
dueDate: Carbon::parse($request->input('due_date')),
);
}
}
DDD Ne Zaman Uygulanmalı (Ve Ne Zaman Uygulanmamalı)
- DDD kullanın: İş mantığınız karmaşık olduğunda, birden fazla kural içerdiğinde ve sık değiştiğinde
- DDD'yi atlayın: Basit CRUD uygulamaları, prototipler veya yönetim panelleri için
- Basit başlayın -- karmaşıklık arttıkça her zaman DDD'ye doğru yeniden yapılandırabilirsiniz
- Her DDD desenini uygulamayın -- gerçek sorunlarınızı çözen desenleri seçin
"DDD'nin amacı kitaptaki her deseni kullanmak değildir. Amaç, geliştiriciler ve alan uzmanları arasında alan hakkında ortak bir anlayış oluşturmak ve bu anlayışı kodda yansıtmaktır."