بذر البيانات ومصانع النماذج في Laravel
Seeder وFactory هما "مولّدا البيانات" في Laravel — بأمر واحد، يمكنك توليد آلاف سجلات بيانات الاختبار الواقعية، مما يحول بيئة التطوير فورًا إلى سيناريو واقعي.
1. ما ستتعلمه
- تعريف مصنع النموذج: توليد بيانات Faker وتعديل الحالة
- كود Seeder وسلسلة الاستدعاء: $this->call()
- مصنع البيانات المرتبطة: عند إنشاء مستخدم، يُنشأ طلب ومنتج في نفس الوقت
- عزل بيانات المستأجرين المتعددين: TenantSeeder يعبئ البيانات حسب المستأجر
- سير عمل
php artisan db:seedوmigrate:fresh --seed
2. قصة حقيقية من مختبر
(1) المشكلة: توليد البيانات يدويًا أبطأ من كتابة الكود
في كل مرة تختبر Alice تطبيق ShopMetrics، عليها إنشاء مستأجرين يدويًا وإضافة متاجر وإدخال منتجات وتقديم طلبات — يستغرق 30 دقيقة لتوليد 10 سجلات اختبار. Bob أسوأ حالًا؛ كان يحتاج لاختبار ميزة التقسيم، لكن بعد إدخال 100 سجل يدويًا، تعطل متصفحه وفقد جميع البيانات. حاول Charlie كتابة نص SQL لتعبئة البيانات، لكن العلاقات كانت معقدة جدًا (مستأجر ← متجر ← منتج ← طلب ← بند طلب). كتب 500 سطر SQL وما زال يفقد مفتاحًا أجنبيًا.
(2) نهج Factory + Seeder
يستخدم مصانع Laravel بـ Faker لتوليد بيانات واقعية تلقائيًا؛ ينسق Seeders ترتيب التعبئة، وتُنجز البيانات المرتبطة بسطر واحد من الكود.
// المصنع يولد بيانات واقعية
$shop = Shop::factory()->create();
$shop->products()->createMany(
Product::factory()->count(10)->make()->toArray()
);
// متجر واحد + 10 منتجات في سطرين
(3) العائد
بعد استخدام Factory، ولّدت Alice 1000 سجل اختبار (بما في ذلك جميع العلاقات) في 30 ثانية، واختبار التقسيم احتاج فقط migrate:fresh --seed.
3. مصنع النموذج
(1) إنشاء مصنع
php artisan make:factory ShopFactory
# أو مع نموذج
php artisan make:factory ShopFactory --model=Shop
(2) تعريف مصنع
// database/factories/ShopFactory.php
class ShopFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'description' => fake()->paragraph(),
'status' => fake()->randomElement(['active', 'suspended', 'closed']),
'revenue' => fake()->randomFloat(2, 100, 100000),
];
}
}
(3) استراتيجيات Faker الشائعة
| الطريقة | مثال على الإخراج | الوصف |
|---|---|---|
name() |
"Alice Johnson" | الاسم |
email() |
"alice@example.com" | البريد الإلكتروني |
company() |
"Acme Corp" | اسم الشركة |
slug() |
"acme-corp" | slug لعنوان URL |
paragraph() |
"Lorem ipsum..." | نص فقرة |
randomFloat(2, 0, 100) |
45.67 | رقم عشري |
numberBetween(1, 100) |
42 | نطاق أعداد صحيحة |
randomElement([...]) |
اختيار عنصر عشوائي من مصفوفة | قيم تعداد |
dateTimeThisYear() |
"June 15, 2024" | تاريخ |
imageUrl() |
"https://via.placeholder.com/640x480" | عنوان URL للصورة |
unique() |
ضمان التفرد | معدّل |
(1) ▶ مثال: تعريف المصنع الأساسي لـ ShopMetrics
// database/factories/TenantFactory.php
class TenantFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'domain' => fake()->unique()->domainName(),
'status' => 'active',
];
}
}
// database/factories/ProductFactory.php
class ProductFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->words(3, true),
'sku' => strtoupper(fake()->unique()->lexify('???-????')),
'price' => fake()->randomFloat(2, 9.99, 999.99),
'stock' => fake()->numberBetween(0, 500),
'is_active' => true,
];
}
}
// database/factories/OrderFactory.php
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'order_number' => fake()->unique()->numerify('ORD-########'),
'subtotal' => fake()->randomFloat(2, 10, 5000),
'discount' => 0,
'total' => fake()->randomFloat(2, 10, 5000),
'status' => fake()->randomElement(['pending', 'processing', 'completed', 'cancelled']),
];
}
}
الناتج:
// تم التنفيذ بنجاح
4. معدّلات حالة المصنع
(1) تعريف الحالات
// database/factories/ShopFactory.php
class ShopFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'status' => 'active',
'revenue' => fake()->randomFloat(2, 100, 50000),
];
}
public function suspended(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'suspended',
'revenue' => 0,
]);
}
public function highRevenue(): static
{
return $this->state(fn (array $attributes) => [
'revenue' => fake()->randomFloat(2, 50000, 500000),
]);
}
}
(2) استخدام الحالة
// متجر نشط (افتراضي)
$shop = Shop::factory()->create();
// متجر معلق
$shop = Shop::factory()->suspended()->create();
// متجر بإيرادات عالية
$shop = Shop::factory()->highRevenue()->create();
// دمج الحالات
$shop = Shop::factory()->highRevenue()->suspended()->create();
| الاستخدام | الوصف |
|---|---|
factory()->create() |
الحالة الافتراضية، مُخزنة في DB |
factory()->make() |
الحالة الافتراضية، غير مُخزنة في DB |
factory()->suspended()->create() |
استخدام حالة "معلق" |
factory()->count(10)->create() |
إنشاء 10 إدخالات |
factory()->for(Tenant::factory()) |
ربط بنموذج أب |
(1) ▶ مثال: تركيبات حالة مصنع ShopMetrics
// database/factories/OrderFactory.php
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'order_number' => fake()->unique()->numerify('ORD-########'),
'subtotal' => fake()->randomFloat(2, 10, 5000),
'discount' => 0,
'total' => fake()->randomFloat(2, 10, 5000),
'status' => 'pending',
];
}
public function completed(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'completed',
]);
}
public function cancelled(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'cancelled',
'total' => 0,
'subtotal' => 0,
]);
}
public function highValue(): static
{
return $this->state(fn (array $attributes) => [
'subtotal' => fake()->randomFloat(2, 1000, 10000),
'total' => fake()->randomFloat(2, 1000, 10000),
]);
}
}
// الاستخدام
$order = Order::factory()->completed()->highValue()->create();
الناتج:
// تم التنفيذ بنجاح
5. مصنع البيانات المرتبطة
(1) إنشاء ارتباط
// إنشاء متجر مع منتجات
$shop = Shop::factory()
->has(Product::factory()->count(10), 'products')
->create();
// أو باستخدام طريقة سحرية
$shop = Shop::factory()
->hasProducts(10)
->create();
// إنشاء طلب لمتجر محدد
$order = Order::factory()
->for($shop)
->create();
// إنشاء مستخدم مع مستأجر
$user = User::factory()
->for(Tenant::factory())
->create();
| الطريقة | الوصف | مثال |
|---|---|---|
has() |
إنشاء ارتباط فرعي | has(Product::factory()->count(5)) |
hasProducts(5) |
اختصار للطريقة السحرية | مكافئ لـ has(Product::factory()->count(5), 'products') |
for() |
ربط بنموذج أب | for($tenant) أو for(Tenant::factory()) |
(1) ▶ مثال: مصنع تكامل بيانات ShopMetrics
// إنشاء نظام بيئي كامل للمستأجر
$tenant = Tenant::factory()
->has(User::factory()->count(3))
->has(Shop::factory()->count(5)->hasProducts(10))
->create();
// إنشاء طلب مع بنود لمتجر
$shop = Shop::factory()->create();
$products = Product::factory()->count(5)->for($shop)->create();
$order = Order::factory()
->for($shop->tenant)
->for($shop)
->for(User::factory()->for($shop->tenant))
->has(OrderItem::factory()->count(3)->state([
'product_id' => $products->random()->id,
]), 'items')
->create();
الناتج:
// تم التنفيذ بنجاح
6. تنسيق Seeder
(1) إنشاء Seeder
php artisan make:seeder TenantSeeder
php artisan make:seeder ProductSeeder
(2) كتابة Seeder
// database/seeders/TenantSeeder.php
class TenantSeeder extends Seeder
{
public function run(): void
{
Tenant::factory()
->count(10)
->has(User::factory()->count(3))
->has(Shop::factory()->count(5)->hasProducts(10))
->create();
}
}
(3) سلسلة الاستدعاء
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
PlanSeeder::class,
TenantSeeder::class,
ProductSeeder::class,
OrderSeeder::class,
]);
}
}
(1) ▶ مثال: DatabaseSeeder الكامل لـ ShopMetrics
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// 1. إنشاء الخطط
Plan::factory()->createMany([
['name' => 'Starter', 'slug' => 'starter', 'price' => 29, 'shop_limit' => 5],
['name' => 'Pro', 'slug' => 'pro', 'price' => 79, 'shop_limit' => 25],
['name' => 'Enterprise', 'slug' => 'enterprise', 'price' => 199, 'shop_limit' => null],
]);
// 2. إنشاء مستأجرين مع بيانات كاملة
Tenant::factory()
->count(20)
->has(User::factory()->count(5))
->has(Shop::factory()->count(3)->hasProducts(15))
->create();
// 3. إنشاء طلبات لكل متجر
Shop::all()->each(function ($shop) {
Order::factory()
->count(50)
->for($shop->tenant)
->for($shop)
->for($shop->tenant->users->random())
->create();
});
}
}
الناتج:
// تم التنفيذ بنجاح
7. تعبئة عزل بيانات المستأجرين المتعددين
(1) تخصيص المستأجرين الفرعيين
// database/seeders/TenantIsolatedSeeder.php
class TenantIsolatedSeeder extends Seeder
{
public function run(): void
{
$tenants = Tenant::factory()->count(10)->create();
foreach ($tenants as $tenant) {
// كل مستأجر يحصل على بيانات معزولة
$users = User::factory()->count(5)->for($tenant)->create();
$shops = Shop::factory()->count(3)->for($tenant)->create();
foreach ($shops as $shop) {
$products = Product::factory()->count(20)->for($shop)->create();
// إنشاء طلبات مع بنود
Order::factory()->count(30)->for($tenant)->for($shop)
->for($users->random())
->create()
->each(function ($order) use ($products) {
$orderItems = $products->random(rand(1, 5));
foreach ($orderItems as $product) {
$order->items()->create([
'product_id' => $product->id,
'quantity' => rand(1, 3),
'price' => $product->price,
]);
}
$order->update([
'subtotal' => $order->items->sum(fn ($i) => $i->price * $i->quantity),
'total' => $order->items->sum(fn ($i) => $i->price * $i->quantity),
]);
});
}
}
}
}
(2) تخطيط حجم البيانات
| الكيان | لكل مستأجر | الإجمالي لـ 10 مستأجرين | ملاحظات |
|---|---|---|---|
| المستخدمون | 5 | 50 | بما في ذلك مالك واحد |
| المتاجر | 3 | 30 | جميعها نشطة |
| المنتجات | 20/متجر | 600 | نشطة/غير نشطة عشوائية |
| الطلبات | 30/متجر | 900 | حالات متنوعة |
| بنود الطلبات | 1-5 لكل طلب | ~2,700 | كمية عشوائية |
(1) ▶ مثال: تشغيل سير عمل التعبئة
# تهجير جديد + بذر (تطوير)
php artisan migrate:fresh --seed
# تشغيل بذر محدد
php artisan db:seed --class=TenantSeeder
# تشغيل بكمية مخصصة
php artisan db:seed --class=OrderSeeder
# الإنتاج — لا تستخدم migrate:fresh أبدًا!
php artisan migrate --force
php artisan db:seed --force
الناتج:
# تم تنفيذ الأمر بنجاح
8. مثال شامل: تعبئة بيانات كاملة لـ ShopMetrics
// ============================================
// شامل: Seeder كامل لـ ShopMetrics
// يغطي: المصانع، الحالات، العلاقات، عزل المستأجر
// ============================================
// database/seeders/ShopMetricsSeeder.php
class ShopMetricsSeeder extends Seeder
{
public function run(): void
{
// الخطوة 1: الخطط
$starter = Plan::create(['name' => 'Starter', 'slug' => 'starter', 'price' => 29, 'shop_limit' => 5, 'features' => ['basic_analytics']]);
$pro = Plan::create(['name' => 'Pro', 'slug' => 'pro', 'price' => 79, 'shop_limit' => 25, 'features' => ['advanced_analytics', 'api_access']]);
$enterprise = Plan::create(['name' => 'Enterprise', 'slug' => 'enterprise', 'price' => 199, 'shop_limit' => null, 'features' => ['custom_analytics', 'api_access', 'sso']]);
// الخطوة 2: إنشاء مدير عام
User::factory()->create(['email' => 'admin@shopmetrics.io', 'role' => 'super_admin']);
// الخطوة 3: إنشاء مستأجرين مع بيانات معزولة
Tenant::factory()->count(20)->create()->each(function ($tenant) use ($starter, $pro, $enterprise) {
// تعيين خطة عشوائية
$plan = fake()->randomElement([$starter, $pro, $enterprise]);
Subscription::create(['tenant_id' => $tenant->id, 'plan_id' => $plan->id, 'status' => 'active']);
// إنشاء مالك المستأجر
$owner = User::factory()->for($tenant)->create(['role' => 'tenant_owner']);
// إنشاء محللين
$analysts = User::factory()->count(3)->for($tenant)->create(['role' => 'analyst']);
// إنشاء متاجر مع منتجات
$shopCount = min(fake()->numberBetween(2, 5), $plan->shop_limit ?? 99);
Shop::factory()->count($shopCount)->for($tenant)->create()->each(function ($shop) use ($tenant, $owner, $analysts) {
// منتجات لكل متجر
Product::factory()->count(fake()->numberBetween(10, 30))->for($shop)->create();
// طلبات لكل متجر
Order::factory()->count(fake()->numberBetween(20, 80))
->for($tenant)
->for($shop)
->for(fake()->randomElement(array_merge([$owner], $analysts->all())))
->create()
->each(function ($order) use ($shop) {
$products = $shop->products()->inRandomOrder()->take(rand(1, 5))->get();
foreach ($products as $product) {
$qty = rand(1, 3);
$order->items()->create([
'product_id' => $product->id,
'quantity' => $qty,
'price' => $product->price,
]);
}
$subtotal = $order->items->sum(fn ($i) => $i->price * $i->quantity);
$order->update(['subtotal' => $subtotal, 'total' => $subtotal]);
});
});
});
}
}
❓ أسئلة شائعة
db:seed.make() وcreate()؟make() ينشئ نسخة نموذج لكن لا يحفظها في قاعدة البيانات؛ يُستخدم في سيناريوهات لا تتطلب استمرارية. create() ينشئ نسخة النموذج ويحفظها في قاعدة البيانات؛ يُستخدم في سيناريوهات تتطلب معرفًا واستعلامات مرتبطة.fake('ar_SA')->name()، أو استخدم fake()->locale('ar_SA') في Factory. ومع ذلك، نوصي بالاحتفاظ بالبيانات بالإنجليزية لأغراض التدويل.DB::table('products')->insert($data). عطّل أحداث النموذج: Product::withoutEvents(fn () => Product::factory()->count(1000)->create()).plans)؛ لا تستخدم Factory لتوليد بيانات وهمية. تأكد من التحقق من الأمر عند إضافة علامة --force.$shop = Shop::factory()->create(). يسترجع Laravel معاملة قاعدة البيانات تلقائيًا بعد كل اختبار، لذا الاختبارات لا تؤثر على بعضها البعض.📖 ملخص
- Factory يحدد قواعد توليد البيانات؛ Faker يوفر بيانات عشوائية واقعية
- تعديل الحالة (state) يحدد سيناريوهات بيانات مختلفة لنفس النموذج
- استخدم
has()وfor()لإنشاء بيانات مرتبطة في مصانع الارتباطات؛ الطريقة السحريةhasProducts()تبسط البنية - ينسق Seeder ترتيب التعبئة، وDatabaseSeeder يستدعي Seeders الفرعية
- لبيانات المستأجرين المتعددين، عبّئ البيانات حسب المستأجر وتأكد من ربط
tenant_idبشكل صحيح - استخدم
migrate:fresh --seedفي التطوير؛ استخدم فقطdb:seed --forceفي الإنتاج
📝 تمارين
-
تمرين أساسي (⭐): أنشئ ShopFactory وProductFactory لـ ShopMetrics، استخدم Tinker لتوليد 3 متاجر مع 5 منتجات لكل منها، وتحقق من أن البيانات المرتبطة صحيحة.
-
تمرين متقدم (⭐⭐): اكتب
TenantIsolatedSeederيولد 5 مستأجرين، كل منهم يحتوي على متجرين و10 منتجات و20 طلبًا، باستخدام طرق ارتباط المصنع (has/for) لإنشاء البيانات. -
تحدٍ (⭐⭐⭐): نفذ حل بذر بيانات كامل يراعي الحالة — أضف ثلاث حالات (نشط، معلق، وإيرادات عالية) إلى
ShopFactory، وثلاث حالات (مكتمل، ملغي، ومسترد) إلىOrderFactory، ووزعها بنسبيًا في Seeder (80% مكتمل، 15% ملغي، 5% مسترد).



