404 Not Found

404 Not Found


nginx

تمرين شامل للمرحلة الثانية—نظام CRUD الخلفي لـ ShopMetrics

يركز التمرين الشامل للمرحلة الثانية على "تسليم الوظائف الأساسية"—جمع كل المعارف من الدروس 8-13 لبناء نظام خلفي يعمل بالكامل.

1. ما ستتعلمه


2. قصة تسليم Bob للمرحلة الثانية

(1) نقطة الألم: الميزات الخلفية متفرقة عبر دروس مختلفة، مما يجعل من المستحيل تكوين منتج كامل

أكملت Alice جميع دروس المرحلة الثانية، لكن الترحيل في الدرس 8، والنموذج في الدرس 9، والتحقق في الدرس 12—كل درس يعمل بشكل جيد بمفرده، لكن عند الجمع بينهم، تظهر مشاكل: قيود المفاتيح الأجنبية غير متطابقة، وعلاقات النماذج مفقودة، وقواعد التحقق لا تتوافق مع أنواع أعمدة قاعدة البيانات.

(2) حلول التمارين الشاملة

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

BASH
# مخرج المرحلة الثانية: نظام إدارة CRUD كامل
php artisan migrate:fresh --seed
# → أكثر من 10 جداول، أكثر من 100 سجل، جميع عمليات CRUD تعمل

(3) العائد

بعد إكمال التمرين الشامل، أصبح لدى Alice نظام CRUD خلفي كامل الوظائف لـ ShopMetrics—من إنشاء الجداول إلى إدراج وتحديث وحذف والاستعلام عن البيانات، كل ذلك في خطوة واحدة.


3. تصميم قاعدة البيانات

(1) مخطط الكيان-العلاقة متعدد المستأجرين لـ ShopMetrics

100%
erDiagram
    Tenant ||--o{ User : "لديه العديد"
    Tenant ||--o| Subscription : "لديه واحد"
    Tenant ||--o{ Shop : "لديه العديد"
    Plan ||--o{ Subscription : "لديه العديد"
    Shop ||--o{ Product : "لديه العديد"
    Shop ||--o{ Order : "لديه العديد"
    User ||--o{ Order : "يُقدّم"
    Product }o--o{ Category : "ينتمي للعديد"
    Order ||--o{ OrderItem : "يحتوي"
    Product ||--o{ OrderItem : "مُضمَّن في"

(2) قائمة الترحيل

# ملف الترحيل اسم الجدول الحقول الرئيسية
1 create_plans_table plans name, slug, price, shop_limit, features(json)
2 create_tenants_table tenants name, slug, domain, plan_id (FK), status
3 create_subscriptions_table subscriptions tenant_id (FK), plan_id (FK), stripe_id, status
4 create_users_table users tenant_id (FK), name, email, password, role
5 create_shops_table shops tenant_id (FK), name, slug, status, revenue
6 create_categories_table categories name, slug
7 create_products_table products shop_id (FK), name, sku, price, stock, is_active
8 create_category_product_table category_product category_id (FK), product_id (FK)
9 create_orders_table orders tenant_id (FK), shop_id (FK), user_id (FK), total, status
10 create_order_items_table order_items order_id (FK), product_id (FK), qty, price

(1) ▶ مثال: ملف الترحيل الأساسي

PHP
// database/migrations/2024_01_01_000001_create_plans_table.php
Schema::create('plans', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->decimal('price', 8, 2);
    $table->integer('shop_limit')->default(5);
    $table->json('features')->nullable();
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

// database/migrations/2024_01_01_000005_create_shops_table.php
Schema::create('shops', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->string('name');
    $table->string('slug');
    $table->text('description')->nullable();
    $table->enum('status', ['active', 'suspended', 'closed'])->default('active');
    $table->decimal('revenue', 12, 2)->default(0);
    $table->timestamps();
    $table->softDeletes();
    $table->unique(['tenant_id', 'slug']);
    $table->index(['tenant_id', 'status']);
});

الناتج:

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

4. طبقة نموذج Eloquent

(1) قائمة النماذج والعلاقات

النموذج العلاقة النطاق Accessor/Mutator
Tenant shops(), users(), subscription() active() domain_url
Shop products(), orders(), tenant() active() revenue_formatted
Product categories(), shop() active(), inStock() price_formatted
Order items(), shop(), user() completed(), thisMonth() status_badge
OrderItem product(), order() subtotal

(1) ▶ مثال: النموذج الأساسي لـ ShopMetrics

PHP
// app/Models/Tenant.php
class Tenant extends Model
{
    protected $fillable = ['name', 'slug', 'domain', 'plan_id', 'status'];
    protected $casts = ['status' => 'string'];

    public function shops(): HasMany
    {
        return $this->hasMany(Shop::class);
    }

    public function users(): HasMany
    {
        return $this->hasMany(User::class);
    }

    public function subscription(): HasOne
    {
        return $this->hasOne(Subscription::class)->where('status', 'active');
    }

    public function scopeActive(Builder $query): Builder
    {
        return $query->where('status', 'active');
    }

    public function getDomainUrlAttribute(): string
    {
        return 'https://' . $this->domain;
    }
}

// app/Models/Shop.php
class Shop extends Model
{
    use SoftDeletes;

    protected $fillable = ['tenant_id', 'name', 'slug', 'description', 'status', 'revenue'];
    protected $casts = ['revenue' => 'decimal:2', 'deleted_at' => 'datetime'];

    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }

    public function products(): HasMany
    {
        return $this->hasMany(Product::class);
    }

    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }

    public function scopeActive(Builder $query): Builder
    {
        return $query->where('status', 'active');
    }

    public function getRevenueFormattedAttribute(): string
    {
        return '$' . number_format($this->revenue, 2);
    }
}

// app/Models/Product.php
class Product extends Model
{
    protected $fillable = ['shop_id', 'name', 'sku', 'price', 'stock', 'is_active'];
    protected $casts = ['price' => 'decimal:2', 'is_active' => 'boolean'];

    public function shop(): BelongsTo
    {
        return $this->belongsTo(Shop::class);
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class)->withTimestamps();
    }

    public function scopeActive(Builder $query): Builder
    {
        return $query->where('is_active', true);
    }

    public function scopeInStock(Builder $query): Builder
    {
        return $query->where('stock', '>', 0);
    }

    public function getPriceFormattedAttribute(): string
    {
        return '$' . number_format($this->price, 2);
    }
}

الناتج:

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

5. متحكم الموارد CRUD

(1) قائمة المتحكمات وفئات التحقق

المتحكم الطرق طلب النموذج
ShopController index/create/store/show/edit/update/destroy StoreShopRequest/UpdateShopRequest
ProductController index/create/store/show/edit/update/destroy StoreProductRequest/UpdateProductRequest
OrderController index/show/update UpdateOrderRequest

(1) ▶ مثال: CRUD كامل لـ ShopController

PHP
// app/Http/Controllers/ShopController.php
class ShopController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('tenant.resolve');
    }

    public function index(Request $request): View
    {
        $shops = Shop::where('tenant_id', tenant()->id)
            ->withCount(['products', 'orders as recent_orders' => fn ($q) => $q->where('created_at', '>=', now()->subDays(30))])
            ->when($request->search, fn ($q, $s) => $q->where('name', 'like', "%{$s}%"))
            ->when($request->status, fn ($q, $s) => $q->where('status', $s))
            ->orderBy($request->sort ?? 'created_at', $request->direction ?? 'desc')
            ->paginate(15);

        return view('shops.index', compact('shops'));
    }

    public function create(): View
    {
        return view('shops.create');
    }

    public function store(StoreShopRequest $request): RedirectResponse
    {
        $shop = Shop::create(array_merge(
            $request->validated(),
            ['tenant_id' => tenant()->id],
        ));

        return redirect()->route('shops.show', $shop)
            ->with('success', 'Shop created successfully.');
    }

    public function show(Shop $shop): View
    {
        $shop->load(['products' => fn ($q) => $q->active()->latest()->take(10),
                      'orders' => fn ($q) => $q->with('user')->latest()->take(10)]);

        $stats = [
            'total_revenue' => $shop->orders()->completed()->sum('total'),
            'total_orders' => $shop->orders()->count(),
            'total_products' => $shop->products()->active()->count(),
        ];

        return view('shops.show', compact('shop', 'stats'));
    }

    public function edit(Shop $shop): View
    {
        return view('shops.edit', compact('shop'));
    }

    public function update(UpdateShopRequest $request, Shop $shop): RedirectResponse
    {
        $shop->update($request->validated());
        return redirect()->route('shops.show', $shop)
            ->with('success', 'Shop updated.');
    }

    public function destroy(Shop $shop): RedirectResponse
    {
        $shop->delete();
        return redirect()->route('shops.index')
            ->with('success', 'Shop deleted.');
    }
}

الناتج:

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

6. تنفيذ فئات التحقق

(1) ▶ مثال: فئة طلب النموذج لـ ShopMetrics

PHP
// app/Http/Requests/StoreShopRequest.php
class StoreShopRequest extends FormRequest
{
    public function authorize(): bool
    {
        return auth()->user()->role === 'tenant_owner';
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'slug' => 'required|alpha_dash|unique:shops,slug,NULL,id,tenant_id,' . tenant()->id,
            'description' => 'nullable|string|max:5000',
            'status' => 'sometimes|in:active,suspended',
        ];
    }

    protected function prepareForValidation(): void
    {
        $this->merge(['slug' => Str::slug($this->slug ?? $this->name)]);
    }
}

// app/Http/Requests/StoreProductRequest.php
class StoreProductRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'sku' => 'required|string|unique:products,sku',
            'price' => 'required|numeric|min:0.01|max:999999.99',
            'stock' => 'required|integer|min:0',
            'description' => 'nullable|string|max:5000',
            'is_active' => 'boolean',
            'categories' => 'sometimes|array',
            'categories.*' => 'exists:categories,id',
            'image' => 'sometimes|image|max:2048',
        ];
    }
}

الناتج:

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

7. تعبئة البيانات

(1) ▶ مثال: بادئ بيانات ShopMetrics الكامل

PHP
// database/seeders/ShopMetricsSeeder.php
class ShopMetricsSeeder extends Seeder
{
    public function run(): void
    {
        $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']]);
        $enterprise = Plan::create(['name' => 'Enterprise', 'slug' => 'enterprise', 'price' => 199, 'shop_limit' => null, 'features' => ['all']]);

        User::factory()->create(['email' => 'admin@shopmetrics.io', 'role' => 'super_admin']);

        Category::factory()->count(10)->create();

        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) {
                $products = Product::factory()->count(fake()->numberBetween(10, 30))->for($shop)->create();
                $products->each(fn ($p) => $p->categories()->attach(Category::inRandomOrder()->take(rand(1, 3))->pluck('id')));

                Order::factory()->count(fake()->numberBetween(20, 60))->for($tenant)->for($shop)->for(fake()->randomElement(array_merge([$owner], $analysts->all())))->create()->each(function ($order) use ($products) {
                    $items = $products->random(rand(1, 5));
                    foreach ($items as $product) {
                        $qty = rand(1, 3);
                        $order->items()->create(['product_id' => $product->id, 'quantity' => $qty, 'price' => $product->price]);
                    }
                    $order->update(['total' => $order->items->sum(fn ($i) => $i->price * $i->quantity)]);
                });
            });
        });
    }
}

الناتج:

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

8. كشف N+1 وتحسينها

(1) تثبيت Debugbar

BASH
composer require barryvdh/laravel-debugbar --dev

(2) سيناريوهات N+1 الشائعة وحلولها

السيناريو مشكلة N+1 الحل
قائمة المتاجر $shop->products->count() withCount('products')
قائمة الطلبات $order->user->name with('user')
تفاصيل المنتج $product->categories with('categories')
لوحة التحكم الوصول المتزامن لعدة علاقات with(['user', 'shop.items'])

(1) ▶ مثال: تحليل الاستعلامات بـ Debugbar

PHP
// في بيئة التطوير، يعرض Debugbar عدد الاستعلامات في كل صفحة
// تحسين N+1 خطوة بخطوة:

// الخطوة 1: التحديد — يعرض Debugbar "41 استعلام" في shops.index
// الخطوة 2: إضافة التحميل المسبق
$shops = Shop::with(['products', 'tenant', 'orders'])->get(); // 4 استعلامات
// الخطوة 3: استخدام withCount بدلاً من تحميل العلاقات الكاملة
$shops = Shop::withCount('products', 'orders')->with('tenant')->get(); // 3 استعلامات
// الخطوة 4: التحقق — يعرض Debugbar "3 استعلامات"

الناتج:

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

9. مثال شامل: نظام CRUD الخلفي لـ ShopMetrics

PHP
// ============================================
// شامل: إعداد إدارة ShopMetrics
// يغطي: الترحيل، النماذج، المتحكمات، الطلبات، بادئات البيانات
// ============================================

// أوامر الإعداد (نفّذ بالترتيب)
// 1. php artisan migrate:fresh --seed
// 2. php artisan storage:link
// 3. php artisan serve

// routes/web.php — مسارات الإدارة الكاملة
Route::middleware('auth')->prefix('dashboard')->name('dashboard.')->group(function () {
    Route::get('/', [DashboardController::class, 'index'])->name('index');

    // متاجر CRUD
    Route::resource('shops', ShopController::class);
    Route::post('/shops/{shop}/logo', [ShopLogoController::class, 'update'])->name('shops.logo');

    // منتجات CRUD (متداخلة تحت المتجر)
    Route::resource('shops.products', ProductController::class)->shallow();

    // الطلبات (قراءة فقط + تحديث الحالة)
    Route::resource('orders', OrderController::class)->only(['index', 'show', 'update']);
    Route::post('/orders/{order}/export', ExportOrdersController::class)->name('orders.export');

    // الفئات (CRUD بسيط)
    Route::resource('categories', CategoryController::class)->except('show');

    // التحليلات
    Route::get('/analytics', [AnalyticsController::class, 'index'])->name('analytics');
});

// DashboardController مع استعلامات محسّنة
class DashboardController extends Controller
{
    public function index(): View
    {
        $tenant = tenant();
        $stats = Cache::remember("dashboard.{$tenant->id}", 300, function () use ($tenant) {
            return [
                'total_revenue' => $tenant->shops()->sum('revenue'),
                'active_shops' => $tenant->shops()->active()->count(),
                'monthly_orders' => $tenant->orders()->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])->count(),
                'total_products' => Product::whereHas('shop', fn ($q) => $q->where('tenant_id', $tenant->id))->active()->count(),
            ];
        });

        $recentOrders = Order::where('tenant_id', $tenant->id)
            ->with(['shop', 'user'])
            ->latest()
            ->take(5)
            ->get();

        return view('dashboard.index', compact('stats', 'recentOrders'));
    }
}

❓ أسئلة شائعة

س كم يستغرق تمرين المرحلة الثانية؟
ج حوالي 4-6 ساعات. تصميم قاعدة البيانات: ساعة؛ النماذج والعلاقات: ساعة ونصف؛ متحكمات CRUD: ساعة ونصف؛ تعبئة البيانات والتنقيح: ساعة. لا تتخطَّ أي خطوة؛ تحقق من كل خطوة قبل الانتقال إلى التالية.
س كيف أضمن أن ترتيب الترحيل صحيح؟
ج يجب إنشاء الجداول التي تعتمد عليها مفاتيح أجنبية أولاً. تحدد الطوابع الزمنية في أسماء ملفات الترحيل ترتيب التنفيذ—الجداول المُشار إليها تُنشأ أولاً، ثم الجداول التي تشير إليها. بدلاً من ذلك، استخدم php artisan migrate:status للتحقق من الترتيب.
س هناك الكثير من التعليمات البرمجية المكررة في صفحات CRUD. كيف يمكنني تقليلها؟
ج استخدم مكونات Blade لتغليف حقول النماذج (<x-input>، <x-select>)، واستخدم artisan make:controller --resource لإنشاء الكود الأساسي. التكرار طبيعي؛ تأكد من أن الوظائف تعمل بشكل صحيح قبل التفكير في التجريد.
س هل يجب تعطيل Debugbar في بيئة الإنتاج؟
ج يجب تعطيله بالتأكيد. يكشف Debugbar معلومات حساسة (استعلامات SQL، استخدام الذاكرة، قيم الإعدادات). يتم تفعيله فقط عند APP_DEBUG=true؛ في بيئة الإنتاج حيث APP_DEBUG=false، يتم تعطيله تلقائياً.
س ما الفرق بين بيانات Seeder وبيانات الاختبار؟
ج بيانات Seeder تُنشئ البيانات الأساسية لبيئة التطوير (حسابات المسؤول، الخطط)، والتي تُخزن بشكل دائم في قاعدة البيانات؛ بيانات الاختبار تُنشأ باستخدام Factory في PHPUnit ويتم التراجع عنها تلقائياً بعد كل اختبار.
س كيف أتحقق من أن N+1 قد تم تحسينها بالكامل؟
ج يجب أن يساوي عدد الاستعلامات المعروضة في Debugbar 1 (الاستعلام الرئيسي) плюс عدد الوصلات المحملة مسبقاً. على سبيل المثال، with(['user', 'shop']) يجب أن يكون 3 استعلامات. إذا كان هناك أكثر من المتوقع، تحقق من أي تحميلات كسر متروكة.

📖 ملخص


📝 تمارين

  1. مهمة أساسية (⭐): أكمل ترحيل الجداول العشرة في لوحة إدارة ShopMetrics وحدد النماذج. شغّل migrate:fresh --seed للتحقق من إنشاء جميع البيانات بشكل صحيح وعدم وجود أخطاء في المفاتيح الأجنبية.

  2. تمرين متقدم (⭐⭐): نفِّذ وظائف CRUD كاملة لـ Shop و Product (بما في ذلك التحقق من طلب النموذج، تصفية البحث، والتقسيم)، مع التأكد من أن Debugbar لا يعرض أكثر من 5 استعلامات لكل صفحة.

  3. تحدي (⭐⭐⭐): نفِّذ تجميع بيانات محسّن بالتخزين المؤقت للوحة التحكم—خزّن استعلامات الإحصائيات مؤقتاً لمدة 5 دقائق، وامسح التخزين المؤقت تلقائياً عبر أحداث النموذج عند استلام طلب جديد لضمان توازن بين أداء البيانات في الوقت الفعلي وأداء النظام العام.

Web-Tutorial.com

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

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

100%