Uygulamanızı Laravel’den Symfony’ye taşımak zorunda kaldığınızda, bu genellikle beklentilerinizi boşa çıkaran bir durumdur. Belki de ekibin standartları Symfony’den yanadır. Belki bir Messenger ve Doctrine kullanıcısı olan bir firma sizi satın aldı. Ya da belki Eloquent’in global kapsamlarının sayısız hafta sonuna mal olduğunu düşünmeye karar verdiler.
Öngörü geri geldiğinde, birisi “büyük yeniden yazım” cümlesini duyurur ve odada bir sessizlik olur çünkü herkes son büyük yeniden yazımın sonuçlarını hatırlar.
İşte burada, bu durumun çeyrek yıl mı yoksa yıl mı süreceğini belirleyen soru devreye giriyor: İş mantığınızın ne kadarı framework’e bağlı?
Eğer PlaceOrder logiğiniz request() çağırıyorsa, Eloquent modeli üzerinden Order::create() yapıyorsa ve DB::transaction() ile bir işlem açıyorsa, o zaman framework uygulamadır ve bu uygulamayı yeniden yazıyorsunuz. Eğer iş kurallarınız, Laravel’i daha önce duymamış düz PHP sınıflarında yer alıyorsa, o zaman göç bir adaptör değişikliği olup, adaptör değişiklikleri bir rota üzerinden taşınabilir.
Framework’ün Gerçek Mülkü
Framework’ün Gerçek Mülkü
Tipik bir Laravel uygulamasını inceleyince, üç kod kategorisi bulursunuz.
Birincisi, iş mantığınız ve kullanım senaryolarıdır: bir siparişin ne olduğu, sipariş vermenin ne anlama geldiği, ne zaman iptal edilebileceği gibi kurallar. Bu, işin ödeme yaptığı kısımdır ve hiç bir şekilde bir framework’ü içermemelidir.
İkincisi ise dış dünyayı bu mantığa çağrı yapacak şekilde çevrilen yapıdır: kontrolcüler, form istekleri, Eloquent modelleri, kuyruk görevleri, servis sağlayıcıları. Bu yapı, tanımı gereği framework’e özgüdür.
Üçüncüsü ise kodunuzun arayüzler aracılığıyla konuştuğu altyapıdır: veritabanı, kuyruk, e-posta gönderici, ödeme geçidi.
Bir framework göçü yalnızca ikinci kategoriyi etkiler. Sorun şu ki, çoğu Laravel uygulaması birinci ve ikinci kategorilerin birbirine karışmasına izin verir, ta ki onları ayırt edemeyecek hale gelene kadar. Göç maliyeti neredeyse tamamen bu kategorilerin ayrılmasının maliyetidir. Bu ayırmayı ilk önce yapın ve hala Laravel’deyken, Symfony geçişi bir hafta sonu kablolaması kadar basit hale gelir.
Geçiş Yaparken Acı Veren Versiyon
Geçiş Yaparken Acı Veren Versiyon
İşte, tahminin şişmesini sağlayan şekil. Kurallar denetleyici içinde, model bir ActiveRecord olarak veritabanına doğrudan konuşuyor ve framework her satıra işlenmiş durumda.
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
final class OrderController
{
public function store(Request $request)
{
$data = $request->validate([
'customer_id' => 'required|uuid',
'items' => 'required|array|min:1',
]);
$order = Order::create([
'customer_id' => $data['customer_id'],
'status' => 'placed',
]);
foreach ($data['items'] as $item) {
$order->items()->create($item);
}
Mail::to($order->customer)->send(new OrderPlaced($order));
return response()->json($order, 201);
}
}
Burada önemli olan her kural hapis. “Bir siparişin en az bir öğesi olmalıdır” bir doğrulama dizesidir. Toplam asla hesaplanmaz. E-posta gönderimi doğrudan yapılır. Bu geçişi Symfony’ye taşımak için tüm yöntemi yeniden yazmanız gerekir ve yeniden yazmanın riski vardır çünkü kurallar tek başlarına test edileceği bir yerde hiç tanımlanmamıştır.
Kuralları Öncelikle Düz PHP’ye Taşıyın
Kuralları Öncelikle Düz PHP’ye Taşıyın
Symfony’ye dokunmadan önce, mantığı kontrolcülerden çıkarın ve Laravel’e bağımlı olmayan bir kullanım senaryosuna koyun. Bu adım, mevcut Laravel uygulamanızla, üretimde ve testlerle tamamen çalışır halde, göç başlamadan önce gerçekleşir.
declare(strict_types=1);
namespace App\Application\Order;
use App\Application\Port\OrderRepository;
use App\Application\Port\EventBus;
use App\Domain\Order\Order;
use App\Domain\Order\OrderId;
final readonly class PlaceOrder
{
public function __construct(
private OrderRepository $orders,
private EventBus $events,
) {}
public function execute(PlaceOrderInput $in): PlaceOrderOutput
{
$order = Order::place(
OrderId::generate(),
$in->customerId,
$in->items,
);
$this->orders->save($order);
$this->events->publishAll($order->releaseEvents());
return PlaceOrderOutput::fromOrder($order);
}
}
Hiçbir Request, Eloquent yok, DB:: ya da Mail:: yok. OrderRepository ve EventBus, Application\Port ad alanında tanımladığınız arayüzlerdir. Order::place “en az bir öğe” kuralını taşıyor ve toplamı hesaplıyor. Bu dosya, hangi framework altında çalıştığına bağlı kalmadan, bugün Laravel’de ve gelecek ay Symfony’de derlenip birim testlerini geçecektir.
Laravel kontrolcüsü, yalnızca bir çevirmenden oluşur:
namespace App\Http\Controllers;
use App\Application\Order\PlaceOrder;
use App\Application\Order\PlaceOrderInput;
use Illuminate\Http\Request;
final class OrderController
{
public function __construct(
private readonly PlaceOrder $placeOrder,
) {}
public function store(Request $request)
{
$input = PlaceOrderInput::fromArray(
$request->all(),
);
$output = $this->placeOrder->execute($input);
return response()->json($output, 201);
}
}
Aynalama, çağırma, serileştirme. Bir kontrolcünün yapması gereken tek şey budur ve atılacak tek kısım Symfony geldiğinde budur.
Ne Taşınır, Ne Kalır
Ne Taşınır, Ne Kalır
Bir ayrım yapıldıktan sonra, göç bir bakışta okunabilir bir tabloya dönüşür.
Kalacak kısımlar, byte for byte:
-
App\Domain\*— varlıklar, değer nesneleri, alan olayları. Saf PHP. -
App\Application\*— kullanım senaryoları, portlar, DTO’lar. Sadece arayüzlere bağlıdır. - Her iki bileşen için birim testleri yelpazeniz.
Ince adaptörler olarak yeniden yazılması gerekenler:
- Kontrolcüler → Symfony kontrolcüleri ile
#[Route]öznitelikleri. - Form istekleri → aynı ayrıştırma, kontrolcünün içinde veya bir DTO fabrikasında yapılır.
- Kuyruk görevleri → Symfony Messenger mesaj işleyicileri.
- Servis sağlayıcıları →
services.yamlbağlılıkları. - Kalıcılık için kullanılan Eloquent modelleri → aynı
OrderRepositoryarayüzü altında Doctrine havuzları.
Dikkat edilmesi gereken kısım kalıcılıktır. Eğer Eloquent modellerini iş nesneleri olarak kullandıysanız, bunlar iş sektörünüzün devralmaması gereken ActiveRecord alışkanlıklarını taşır. Eloquent modelini, arayüzünüzün arkasında bir kalıcılık detayı olarak tutun, onu alan varlığına eşleyin ve geçiş sırasında aynı arabirimin bir Doctrine uygulamasıyla değiştirin. Kullanım senaryosu asla fark etmez çünkü sadece OrderRepository gördü.
Size Kısım Taşınabilirlik Sağlayacak
Size Kısım Taşınabilirlik Sağlayacak
Tüm uygulamayı tek bir dağıtımda değiştirmiyorsunuz. İki framework’ü aynı domaine arka planda çalıştırıyorsunuz ve rotaları bir grup bir seferde taşıyorsunuz. Bu, strangler-fig desenidir ve Martin Fowler’ın yazısı bunun en net tanımını yapmaktadır.
Teknikleri: Her iki uygulamanın önüne ters bir proxy koyun. Yeni Symfony servisi /api/orders ile ilgilenir. Eski Laravel uygulaması ise her şeyi alır. Nginx yolları path’e göre dağıtır.
location /api/orders {
proxy_pass http://symfony_app;
}
location / {
proxy_pass http://laravel_app;
}
Her iki uygulama, aynı App\Domain ve App\Application kodunu paylaşılan bir Composer paketi ya da paylaşılan bir yol olarak yükler. Bir /api/orders isteği geldiğinde, Symfony kontrolcüsü bir PlaceOrderInput oluşturur, önceki gün Laravel kontrolcüsünün çağırdığıyla aynı PlaceOrder kullanım senaryosunu çağırır ve aynı JSON’u döndürür. Müşteri hiçbir şey görmez. İzleme sisteminiz, farklı bir süreç tarafından sağlanan bir başka yön görülür.
Bir rota grubunu taşıyın, bir gün boyunca panelleri izleyin, sonraki grubunu taşıyın. Eğer bir grup sorun çıkarırsa, Nginx değişikliği ile onu Laravel’e yönlendirmek sadece bir satır ve bir yeniden yükleme gerektirir. 2’de uyanmanız gereken büyük bir kesintiyi yoktur.
Symfony Altında Aynı Kullanım Senaryosunu Kablolama
Symfony Altında Aynı Kullanım Senaryosunu Kablolama
Geçiş edilen istemci, bir kontrolcü ve bir services.yaml bağlılığıdır. Kontrolcü, aynı çekirdek için her ikisini de yansıttığı için aynıdır.
declare(strict_types=1);
namespace App\Ui\Http\Controller;
use App\Application\Order\PlaceOrder;
use App\Application\Order\PlaceOrderInput;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
final class PlaceOrderController
{
public function __construct(
private readonly PlaceOrder $placeOrder,
) {}
#[Route('/api/orders', methods: ['POST'])]
public function __invoke(Request $request): JsonResponse
{
$payload = json_decode(
$request->getContent(),
associative: true,
flags: JSON_THROW_ON_ERROR,
);
$output = $this->placeOrder->execute(
PlaceOrderInput::fromArray($payload),
);
return new JsonResponse($output, 201);
}
}
Bağlama, Symfony’nin konteynerine her portun hangi adaptörle doldurulacağını belirtir:
services:
App\Application\Port\OrderRepository:
class: App\Infrastructure\Doctrine\DoctrineOrderRepository
App\Application\Port\EventBus:
class: App\Infrastructure\Messenger\MessengerEventBus
Laravel’da aynı bağlama, bir servis sağlayıcıda üç $this->app->bind(...) çağrısıyla yapılıyordu. Aynı grafik, farklı bir konfigürasyon dosyası. Kullanım senaryosunun yapıcı fonksiyonu asla değişmedi.
İşlem Nerede Olmalı
İşlem Nerede Olmalı
Birçok kişinin geçiş sırasında sıkışıp kalmasına neden olan bir tuzak vardır. Laravel’de alışkanlık DB::transaction(fn () => ...) kontrolcü gövdesinin etrafında sarılmasıdır. Eğer bu çağrı kullanım senaryonuzun içine yerleşirse, Laravel’in facade’ını domeninize çekmiş olursunuz ve Symfony altında var olmayacaktır.
İşlem sınırını, şu anda çalıştığınız framework’e ait olacak şekilde kenarda tutun. Kullanım senaryosunu, mevcut framework’ün sunduğu şekilde açıp kapatacak küçük bir dekoratör ile sarmalayın: Laravel’de DB::transaction, Symfony’de Doctrine EntityManager. Kullanım senaryosu sadece $this->orders->save($order) çağrısını yapar ve bir dış sınırın çizildiğine güvenmek zorundadır. O dekoratör, yeniden yazılan adaptörlerden biridir. Kullanım senaryosu asla değildir.
İşlem Sırası
İşlem Sırası
Gelen geçiş şu adımlarda gerçekleşir ve yalnızca son iki adım Symfony’yi içerir:
- Kontrolcülerden kullanım senaryolarını çıkarın. Portları arayüzler olarak tanımlayın. Laravel’de kalın.
- İş kurallarını saf alan sınıflarına taşıyın ve adlandırılmış fabrikalar oluşturun. Laravel’de kalın.
- İşlem senaryolarını bellek içindeki adaptörlerle birim testleri yazın. Laravel’de kalın.
- Aynı
DomainveApplicationkodunu paylaşarak Symfony uygulamasını başlatın. - Proxy’nin arkasında bir uç nokta grubunu Symfony’ye yönlendirin. İzleyin. Tekrar edin.
1’den 3’e kadar olan adımlar gerçek iş yüküdür ve bunlar, eğer Symfony göçü iptal edilirse bile kendini amorti eder. Her durumda test edilebilir bir çekirdek elde edersiniz. 4 ve 5’in adımları ise aslında biletin konusuydu ve bu adımlara ulaştığınızda risk ortadan kalkmıştır çünkü önemli kod zaten üretimde çalışmış ve etkilenmemiştir.
Bir framework göçü, çoğu uygulama framework’ün yük taşıyan yapı taşları haline gelmesi nedeniyle büyük bir durum gibi görünmektedir. Önce yükü ondaki unsurlardan alın. Sonra Laravel’i Symfony ile değiştirmek, olması gereken şeydir: kapıları yeniden yazmak, evi tutmak.
Hexagonal mimarinin ana fikri, framework’un kenarda, yalnızca alanınızın tanımladığı portlar aracılığıyla erişilebilir olmasıdır. Kalıcılığı, HTTP’yi ve mesajlaşmayı o kenarda tutun ve Laravel ile Symfony arasında seçim yapmak, bir mimari karar olmaktan çıkıp bir konfigürasyon dosyasına dönüşsün. Bu tersine çevirme, yönelişleri değiştiren uygulamalar için göç oyun kitabıdır.
Kaynak: Orijinal Makale


