404 Not Found

404 Not Found


nginx

بذر البيانات ومصانع النماذج في Laravel

Seeder وFactory هما "مولّدا البيانات" في Laravel — بأمر واحد، يمكنك توليد آلاف سجلات بيانات الاختبار الواقعية، مما يحول بيئة التطوير فورًا إلى سيناريو واقعي.

1. ما ستتعلمه


2. قصة حقيقية من مختبر

(1) المشكلة: توليد البيانات يدويًا أبطأ من كتابة الكود

في كل مرة تختبر Alice تطبيق ShopMetrics، عليها إنشاء مستأجرين يدويًا وإضافة متاجر وإدخال منتجات وتقديم طلبات — يستغرق 30 دقيقة لتوليد 10 سجلات اختبار. Bob أسوأ حالًا؛ كان يحتاج لاختبار ميزة التقسيم، لكن بعد إدخال 100 سجل يدويًا، تعطل متصفحه وفقد جميع البيانات. حاول Charlie كتابة نص SQL لتعبئة البيانات، لكن العلاقات كانت معقدة جدًا (مستأجر ← متجر ← منتج ← طلب ← بند طلب). كتب 500 سطر SQL وما زال يفقد مفتاحًا أجنبيًا.

(2) نهج Factory + Seeder

يستخدم مصانع Laravel بـ Faker لتوليد بيانات واقعية تلقائيًا؛ ينسق Seeders ترتيب التعبئة، وتُنجز البيانات المرتبطة بسطر واحد من الكود.

PHP
// المصنع يولد بيانات واقعية
$shop = Shop::factory()->create();
$shop->products()->createMany(
    Product::factory()->count(10)->make()->toArray()
);
// متجر واحد + 10 منتجات في سطرين

(3) العائد

بعد استخدام Factory، ولّدت Alice 1000 سجل اختبار (بما في ذلك جميع العلاقات) في 30 ثانية، واختبار التقسيم احتاج فقط migrate:fresh --seed.


3. مصنع النموذج

(1) إنشاء مصنع

BASH
php artisan make:factory ShopFactory
# أو مع نموذج
php artisan make:factory ShopFactory --model=Shop

(2) تعريف مصنع

PHP
// 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

PHP
// 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']),
        ];
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

4. معدّلات حالة المصنع

(1) تعريف الحالات

PHP
// 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) استخدام الحالة

PHP
// متجر نشط (افتراضي)
$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

PHP
// 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();

الناتج:

TEXT
// تم التنفيذ بنجاح

5. مصنع البيانات المرتبطة

(1) إنشاء ارتباط

PHP
// إنشاء متجر مع منتجات
$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

PHP
// إنشاء نظام بيئي كامل للمستأجر
$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();

الناتج:

TEXT
// تم التنفيذ بنجاح

6. تنسيق Seeder

(1) إنشاء Seeder

BASH
php artisan make:seeder TenantSeeder
php artisan make:seeder ProductSeeder

(2) كتابة Seeder

PHP
// 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) سلسلة الاستدعاء

PHP
// 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

PHP
// 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();
        });
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

7. تعبئة عزل بيانات المستأجرين المتعددين

(1) تخصيص المستأجرين الفرعيين

PHP
// 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) ▶ مثال: تشغيل سير عمل التعبئة

BASH
# تهجير جديد + بذر (تطوير)
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

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

8. مثال شامل: تعبئة بيانات كاملة لـ ShopMetrics

PHP
// ============================================
// شامل: 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]);
                    });
            });
        });
    }
}

❓ أسئلة شائعة

س ما الفرق بين Factory وSeeder؟
ج Factory يحدد قواعد توليد سجلات بيانات فردية (مثل قالب)، بينما Seeder ينسق ترتيب وكمية توليد البيانات (مثل خط تجميع). يمكن استخدام Factory مباشرة في الاختبارات، بينما Seeder يُشغّل باستخدام أمر db:seed.
س ما الفرق بين make() وcreate()؟
ج make() ينشئ نسخة نموذج لكن لا يحفظها في قاعدة البيانات؛ يُستخدم في سيناريوهات لا تتطلب استمرارية. create() ينشئ نسخة النموذج ويحفظها في قاعدة البيانات؛ يُستخدم في سيناريوهات تتطلب معرفًا واستعلامات مرتبطة.
س كيف أولّد بيانات اختبار عربية؟
ج غيّر لغة Faker إلى fake('ar_SA')->name()، أو استخدم fake()->locale('ar_SA') في Factory. ومع ذلك، نوصي بالاحتفاظ بالبيانات بالإنجليزية لأغراض التدويل.
س ماذا أفعل إذا كان Seeder بطيئًا؟
ج قلل عدد استدعاءات Factory وانتقل للإدراج المجمّع: DB::table('products')->insert($data). عطّل أحداث النموذج: Product::withoutEvents(fn () => Product::factory()->count(1000)->create()).
س هل يمكن استخدام Seeder في بيئة الإنتاج؟
ج نعم، لكن بحذر. في بيئة الإنتاج، استخدم Seeder فقط لإضافة بيانات أولية (مثل جدول plans)؛ لا تستخدم Factory لتوليد بيانات وهمية. تأكد من التحقق من الأمر عند إضافة علامة --force.
س كيف أستخدم Factory في الاختبارات؟
ج استدعِ Factory مباشرة داخل دالة الاختبار: $shop = Shop::factory()->create(). يسترجع Laravel معاملة قاعدة البيانات تلقائيًا بعد كل اختبار، لذا الاختبارات لا تؤثر على بعضها البعض.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ ShopFactory وProductFactory لـ ShopMetrics، استخدم Tinker لتوليد 3 متاجر مع 5 منتجات لكل منها، وتحقق من أن البيانات المرتبطة صحيحة.

  2. تمرين متقدم (⭐⭐): اكتب TenantIsolatedSeeder يولد 5 مستأجرين، كل منهم يحتوي على متجرين و10 منتجات و20 طلبًا، باستخدام طرق ارتباط المصنع (has/for) لإنشاء البيانات.

  3. تحدٍ (⭐⭐⭐): نفذ حل بذر بيانات كامل يراعي الحالة — أضف ثلاث حالات (نشط، معلق، وإيرادات عالية) إلى ShopFactory، وثلاث حالات (مكتمل، ملغي، ومسترد) إلى OrderFactory، ووزعها بنسبيًا في Seeder (80% مكتمل، 15% ملغي، 5% مسترد).

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%