Laravel 8: model factory'leri ve yeni dizin yapısı
Laravel 8'in sınıf tabanlı factory yapısını ve yeni dizin düzenini, test ve tohum verisi üretimi odağıyla anlatıyorum.
Laravel 8 Eylül 2020’de çıktı ve bu sürümün benim için en dikkat çekici değişikliği model factory’lerindeydi. Eski factory sistemi işlevsel ama hantal bir şeydi: global fonksiyonlar, sihirli string referanslar, tip güvenliği yok. Yeni sistem bunları sınıf tabanlı bir yapıya taşıdı.
Bu yazıyı özellikle factory konusuna odakladım çünkü test ve seeder verisi üretmek, büyüyen projelerde giderek daha önemli bir konu haline geliyor. İyi bir factory yapısı bu işi gerçekten sürdürülebilir kılıyor.
Eski ve yeni factory karşılaştırması
Laravel 7 ve öncesinde factory tanımı şöyleydi:
// database/factories/UserFactory.php (eski stil)
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'password' => bcrypt('password'),
];
});
Kullanımı global bir fonksiyonla:
$user = factory(App\User::class)->create();
Laravel 8’de factory bir sınıf:
// database/factories/UserFactory.php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'password' => bcrypt('password'),
];
}
}
Kullanımda ise model üzerinden:
$user = User::factory()->create();
Eski sistemde IDE, factory(App\User::class) çağrısından ne döneceğini bilemiyordu — string bir sınıf adını çalışma zamanında çözüyordu. Yeni sistemde User::factory() tipli bir nesne döndürüyor; IDE metodları görüyor, otomatik tamamlama çalışıyor. Küçük bir ayrıntı gibi görünüyor ama büyük factory dosyalarında bu fark günlük yazım hızına yansıyor.
State yönetimi
Yeni sistemin en değerli özelliği state metoduyla farklı durumlar tanımlamak:
public function suspended(): static
{
return $this->state(fn (array $attributes) => [
'suspended_at' => now(),
'suspended_reason' => 'Policy violation',
]);
}
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'role' => 'admin',
]);
}
Kullanımda zincirlenebilir:
// Askıya alınmış admin
$user = User::factory()->suspended()->admin()->create();
// On tane aktif kullanıcı
$users = User::factory()->count(10)->create();
Bu yapı olmadan aynı modelin farklı durumlarını test etmek için ya ayrı factory dosyaları yazılıyordu ya da her testte elle attribute geçiriliyordu. İkisi de bakımı zorlaştırıyor.
İlişkili modeller
Gerçek projelerde factory’lerin en çok iş gördüğü yer ilişkili veriler. Post oluşturulurken kullanıcı da oluşturulsun:
// PostFactory.php
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => $this->faker->sentence(),
'body' => $this->faker->paragraphs(3, true),
'published' => true,
];
}
user_id değeri olarak başka bir factory verildiğinde Laravel onu otomatik çözüyor. Test içinde:
// Var olan bir kullanıcıya ait yazı
$post = Post::factory()->for($existingUser)->create();
// Yorumlarla birlikte yazı
$post = Post::factory()
->has(Comment::factory()->count(5))
->create();
Bir tuzak: for() veya has() kullanırken factory’lerin birbirini recursive olarak çağırmamasına dikkat etmek gerekiyor. Örneğin PostFactory içinde User::factory() kullanıyorsanız ve UserFactory içinde Post::factory() kullanıyorsanız, Post::factory()->create() çağrısı sonsuz döngüye girebilir. Bunu önlemek için for() ile mevcut bir nesneyi geçmek veya ilişkiyi yalnızca bir yönde kurmak yeterli.
Yeni dizin yapısı
Laravel 8, app/Models dizinini varsayılan olarak getirdi. Önceki sürümlerde modeller doğrudan app/ altındaydı; app/User.php, app/Post.php gibi. Bu küçük ama zamanla birikimli bir karmaşa yaratıyordu.
Yeni yapıda modeller app/Models/ altında:
app/
Models/
User.php
Post.php
Comment.php
Http/
Controllers/
Requests/
Mevcut projeleri bu yapıya zorla taşımak gerekmiyor; Laravel 8 geriye dönük uyumlu. Yeni projeler için doğal yer artık bu.
Seeder ile factory birlikteliği
Seeder dosyalarında factory’leri kullanmak test ortamını hızla oluşturuyor:
// database/seeders/DatabaseSeeder.php
public function run(): void
{
User::factory()
->count(10)
->has(Post::factory()->count(5)->has(Comment::factory()->count(3)))
->create();
}
Bu seeder çalıştığında 10 kullanıcı, her birinde 5 yazı, her yazıda 3 yorum oluşuyor. Tek komut: php artisan db:seed.
Bunu eski sistemle karşılaştırdığımda fark büyük. Eski sistemde aynı veriyi üretmek için ya çok daha uzun bir seeder yazmak ya da manuel döngüler kurmak gerekiyordu. Yeni sistem, iç içe ilişkileri zincirleme sözdiziminde ifade etmeye izin veriyor; seeder okunduğunda veri yapısı hemen anlaşılıyor.
Laravel 8’in factory değişikliği görünürde küçük bir refaktör, pratikte büyük bir kazanç. Tip güvenliği, IDE desteği, zincirlenebilir durum tanımları — bunların toplamı test verisi yazmayı gerçekten keyifli hale getiriyor. Bu güncellemeyi iyi bir sinyalin yansıması olarak görüyorum: framework olgunlaşıyor, küçük tutarsızlıkları gideriyor.
Test verisi üretmek, testin yarısı kadar önemli. Karmaşık iç ilişkilere sahip bir model için test kurulmaya başlandığında, eski sistemde veri hazırlamak bazen testin kendisinden daha uzun sürebiliyordu. Yeni factory sistemi bu engeli kaldırıyor: test ne yapmak istediğini anlatıyor, veri hazırlığı birkaç zincir çağrısına indirgeniyor. Bu sadece hız değil; testin okunabilirliği de artıyor — veriyi nasıl kurduğunuz değil, ne test ettiğiniz ön plana çıkıyor.