Laravel'de eylem (action) sınıflarıyla controller'ı inceltmek
İş mantığını controller'dan tek amaçlı action sınıflarına taşıyarak nasıl daha test edilebilir ve bakımı kolay kod yazılır.
Bir Laravel projesinde zamanla en çok şişen yer controller’lar oluyor. Tek bir metot yüzlerce satıra ulaşıyor; doğrulama, iş mantığı, bildirim gönderimi, model güncellemesi hepsi iç içe geçiyor. Bunu daha küçük parçalara ayırmanın birden fazla yolu var. Ben son dönemde action sınıflarını tercih ediyorum — bu yazıda neden ve nasıl kullandığımı anlatacağım.
Sorun ne
Controller’lar HTTP katmanının kapısıdır: isteği alır, işler, yanıt döner. Ama “işler” kısmı büyüyünce controller, HTTP’nin çok ötesine geçen bir sorumluluk yükleniyor. Bu iki sorunu beraberinde getiriyor.
Birincisi, aynı iş mantığını farklı yerden tetiklemek istediğinizde kopyalarsınız. Bir kullanıcı kaydı hem web formuyla hem API ile hem de bir admin komutuyla tetiklenebilir. İş mantığı controller’daysa üç ayrı yere yazarsınız ya da karmaşık bir kalıtım zinciri kurarsınız.
İkincisi, controller metodunu test etmek bir HTTP isteği taklit etmek demektir. Basit bir iş kuralını test etmek için tüm framework önyükleme maliyetini taşırsınız.
Bir projede OrderController@store metodunun 180 satıra ulaştığını gördüğümde durdum. Stok kontrolü, toplam hesaplama, sipariş oluşturma, fatura üretme, bildirim gönderme — bunların hepsi tek metodun içindeydi. Yeni bir tetikleyici (örneğin bir webhook’tan gelen sipariş) eklemek istediğimde kodu tekrarlamak yerine başka bir çözüm aramaya başladım.
Action sınıfı nedir
Action sınıfı (action class), tek bir iş operasyonunu kapsayan sade bir PHP sınıfıdır. Framework’e bağımlılığı minimaldür; çoğunlukla tek bir execute veya handle metodu içerir.
<?php
namespace App\Actions;
use App\Models\User;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
class CreateOrderAction
{
public function execute(User $user, array $items): Order
{
return DB::transaction(function () use ($user, $items) {
$order = $user->orders()->create([
'status' => 'pending',
'total' => collect($items)->sum('price'),
]);
foreach ($items as $item) {
$order->items()->create($item);
}
return $order;
});
}
}
Controller bu sınıfı kullanır, ama iş mantığını bilmez:
<?php
namespace App\Http\Controllers;
use App\Actions\CreateOrderAction;
use App\Http\Requests\CreateOrderRequest;
class OrderController extends Controller
{
public function store(CreateOrderRequest $request, CreateOrderAction $action)
{
$order = $action->execute(
$request->user(),
$request->validated('items')
);
return response()->json($order, 201);
}
}
Laravel’in servis konteyneri (service container) CreateOrderAction’ı otomatik enjekte ediyor. Ek bir yapılandırma gerekmez.
Nereye kadar faydalı
Action sınıfları her şeyi çözmez. Basit CRUD işlemleri için ekstra bir sınıf açmak gereksiz karmaşıklık katıyor. Aşağıdaki durumlar iyi bir eşik:
- Aynı iş mantığının birden fazla girişten tetiklenmesi gerekiyorsa (web, API, artisan komutu).
- İş mantığı 3-4 adımdan fazlaysa ve bunların bağımsız test edilmesi gerekiyorsa.
- İleride bu mantığın queue ile çalıştırılması ihtimali varsa.
Tek adımlı basit işlemler için controller içinde kalmak daha net.
Bir tuzak da şu: bazı geliştiriciler action sınıflarını abartıp UpdateUserEmailAction, UpdateUserNameAction, UpdateUserPhoneAction gibi çok ince parçalar açıyor. Bu gereksiz bir granülarite. “Bu mantık controller dışında bir yerden de tetiklenebilir mi?” sorusuna “evet” cevabı veremiyorsanız, action sınıfı açmak için henüz erken.
Test avantajı somut
Action sınıfını test etmek için HTTP katmanına gerek yok:
<?php
use App\Actions\CreateOrderAction;
use App\Models\User;
it('creates order with correct total', function () {
$user = User::factory()->create();
$action = new CreateOrderAction();
$items = [
['name' => 'Ürün A', 'price' => 100],
['name' => 'Ürün B', 'price' => 50],
];
$order = $action->execute($user, $items);
expect($order->total)->toBe(150)
->and($order->items()->count())->toBe(2);
});
Bu test çok daha hızlı çalışır ve sadece ilgilendiği şeyi test eder.
Alternatiflerle karşılaştırma
Service sınıfları benzer bir amaca hizmet eder ama genellikle çok sayıda metot barındırır. UserService içinde create, update, delete, resetPassword bir arada bulunur. Bu sınıflar zamanla şişer. Action sınıfları single responsibility principle’ı daha sıkı uygular.
Form Request ile bir kısmı çözülebilir, ama Form Request’in amacı yalnızca doğrulama ve yetkilendirme. İş mantığını oraya taşımak kendi problemini yaratır.
Repository deseni veritabanı erişimini soyutlamak için ayrı bir katman. Bunlar birbirine rakip değil; Action sınıfları repository ile birlikte kullanılabilir.
Dosya organizasyonu
App\Actions altında düz bir liste tutmak yerine, büyüyen projelerde etki alanına göre gruplama daha sürdürülebilir oluyor:
App/Actions/
├── Orders/
│ ├── CreateOrderAction.php
│ ├── CancelOrderAction.php
│ └── RefundOrderAction.php
└── Users/
├── RegisterUserAction.php
└── DeactivateUserAction.php
Bu yapı, ekibe yeni biri katıldığında “siparişle ilgili iş mantığı nerede?” sorusuna anında yanıt veriyor. Controller’ın içinde aramaya gerek kalmıyor.
Sonuçta bu bir araç, bir dinsel kural değil. Projede tutarlı bir yer kaydediyorsa ve işi gerçekten daha kolay yapıyorsa kullanılır. Yoksa ek karmaşıklık olmaktan öteye geçmez.