تطوير واجهات REST API

بناء واجهة برمجة تطبيقات RESTful كاملة - الجزء 1

20 دقيقة الدرس 33 من 35

بناء واجهة برمجة تطبيقات RESTful كاملة - الجزء 1

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

نظرة عامة على المشروع

سنبني واجهة برمجة تطبيقات لمنصة تجارة إلكترونية بالميزات التالية:

  • إدارة المستخدمين: التسجيل، المصادقة، الملفات الشخصية
  • كتالوج المنتجات: المنتجات، الفئات، العلامات، البحث
  • سلة التسوق: إضافة/إزالة العناصر، الكميات
  • الطلبات: الدفع، سجل الطلبات، تتبع الحالة
  • المراجعات: تقييمات ومراجعات المنتجات
  • لوحة الإدارة: إدارة المنتجات، الطلبات، المستخدمين

التخطيط: نقاط نهاية واجهة برمجة التطبيقات

مبدأ تصميم واجهة برمجة التطبيقات: صمم نقاط نهاية واجهة برمجة التطبيقات قبل كتابة الكود. فكر في الموارد والعلاقات والعمليات. وثق كل شيء.

لنحدد نقاط نهاية واجهة برمجة التطبيقات:

نقاط نهاية المصادقة:
POST   /api/v1/auth/register          # تسجيل مستخدم جديد
POST   /api/v1/auth/login             # تسجيل دخول المستخدم
POST   /api/v1/auth/logout            # تسجيل خروج المستخدم
POST   /api/v1/auth/refresh           # تحديث الرمز
GET    /api/v1/auth/me                # الحصول على المستخدم المصادق عليه
PUT    /api/v1/auth/profile           # تحديث الملف الشخصي
POST   /api/v1/auth/password/forgot   # نسيت كلمة المرور
POST   /api/v1/auth/password/reset    # إعادة تعيين كلمة المرور
نقاط نهاية المنتجات:
GET    /api/v1/products               # قائمة المنتجات (مع الفلاتر)
GET    /api/v1/products/{id}          # الحصول على تفاصيل المنتج
POST   /api/v1/products               # إنشاء منتج (مسؤول)
PUT    /api/v1/products/{id}          # تحديث منتج (مسؤول)
DELETE /api/v1/products/{id}          # حذف منتج (مسؤول)
GET    /api/v1/products/{id}/reviews  # الحصول على مراجعات المنتج
POST   /api/v1/products/{id}/reviews  # إضافة مراجعة
نقاط نهاية الفئات:
GET    /api/v1/categories             # قائمة جميع الفئات
GET    /api/v1/categories/{id}        # الحصول على تفاصيل الفئة
POST   /api/v1/categories             # إنشاء فئة (مسؤول)
PUT    /api/v1/categories/{id}        # تحديث فئة (مسؤول)
DELETE /api/v1/categories/{id}        # حذف فئة (مسؤول)
GET    /api/v1/categories/{id}/products # المنتجات في الفئة
نقاط نهاية السلة:
GET    /api/v1/cart                   # الحصول على سلة المستخدم
POST   /api/v1/cart/items             # إضافة عنصر إلى السلة
PUT    /api/v1/cart/items/{id}        # تحديث عنصر السلة
DELETE /api/v1/cart/items/{id}        # إزالة من السلة
DELETE /api/v1/cart                   # مسح السلة
نقاط نهاية الطلبات:
GET    /api/v1/orders                 # قائمة طلبات المستخدم
GET    /api/v1/orders/{id}            # الحصول على تفاصيل الطلب
POST   /api/v1/orders                 # إنشاء طلب (الدفع)
PUT    /api/v1/orders/{id}/cancel     # إلغاء الطلب
GET    /api/v1/admin/orders           # قائمة جميع الطلبات (مسؤول)
PUT    /api/v1/admin/orders/{id}      # تحديث حالة الطلب (مسؤول)

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

لنصمم مخطط قاعدة البيانات مع العلاقات:

جدول المستخدمين:
users
├── id (المفتاح الأساسي)
├── name
├── email (فريد)
├── password
├── phone
├── role (enum: user, admin)
├── email_verified_at
├── created_at
└── updated_at
جدول الفئات:
categories
├── id (المفتاح الأساسي)
├── name
├── slug (فريد)
├── description
├── image
├── parent_id (قابل للإلغاء، مرجع ذاتي)
├── is_active
├── created_at
└── updated_at
جدول المنتجات:
products
├── id (المفتاح الأساسي)
├── category_id (مفتاح خارجي)
├── name
├── slug (فريد)
├── description
├── price (عشري)
├── sale_price (عشري، قابل للإلغاء)
├── sku (فريد)
├── stock_quantity
├── is_active
├── is_featured
├── created_at
└── updated_at
جدول صور المنتجات:
product_images
├── id (المفتاح الأساسي)
├── product_id (مفتاح خارجي)
├── image_path
├── is_primary
├── order
├── created_at
└── updated_at
جدول السلات:
carts
├── id (المفتاح الأساسي)
├── user_id (مفتاح خارجي، قابل للإلغاء)
├── session_id (للسلات الضيف)
├── created_at
└── updated_at

cart_items
├── id (المفتاح الأساسي)
├── cart_id (مفتاح خارجي)
├── product_id (مفتاح خارجي)
├── quantity
├── price (لقطة عند وقت الإضافة)
├── created_at
└── updated_at
جدول الطلبات:
orders
├── id (المفتاح الأساسي)
├── user_id (مفتاح خارجي)
├── order_number (فريد)
├── status (enum: pending, processing, completed, cancelled)
├── subtotal
├── tax
├── shipping
├── total
├── payment_method
├── payment_status
├── shipping_address (JSON)
├── billing_address (JSON)
├── notes
├── created_at
└── updated_at

order_items
├── id (المفتاح الأساسي)
├── order_id (مفتاح خارجي)
├── product_id (مفتاح خارجي)
├── product_name (لقطة)
├── quantity
├── price (لقطة)
├── subtotal
├── created_at
└── updated_at
جدول المراجعات:
reviews
├── id (المفتاح الأساسي)
├── product_id (مفتاح خارجي)
├── user_id (مفتاح خارجي)
├── rating (1-5)
├── title
├── comment
├── is_verified_purchase
├── is_approved
├── created_at
└── updated_at

إنشاء الترحيلات

لننشئ ترحيلات قاعدة البيانات:

إنشاء ترحيل المستخدمين:
<?php
# موجود بالفعل في Laravel

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->string('phone')->nullable();
            $table->enum('role', ['user', 'admin'])->default('user');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};
database/migrations/xxxx_create_categories_table.php:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->string('image')->nullable();
            $table->foreignId('parent_id')
                ->nullable()
                ->constrained('categories')
                ->nullOnDelete();
            $table->boolean('is_active')->default(true);
            $table->timestamps();

            $table->index(['slug', 'is_active']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};
database/migrations/xxxx_create_products_table.php:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->foreignId('category_id')
                ->constrained()
                ->cascadeOnDelete();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->decimal('price', 10, 2);
            $table->decimal('sale_price', 10, 2)->nullable();
            $table->string('sku')->unique();
            $table->integer('stock_quantity')->default(0);
            $table->boolean('is_active')->default(true);
            $table->boolean('is_featured')->default(false);
            $table->timestamps();

            $table->index(['slug', 'is_active']);
            $table->index(['category_id', 'is_active']);
        });

        Schema::create('product_images', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')
                ->constrained()
                ->cascadeOnDelete();
            $table->string('image_path');
            $table->boolean('is_primary')->default(false);
            $table->integer('order')->default(0);
            $table->timestamps();

            $table->index(['product_id', 'is_primary']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('product_images');
        Schema::dropIfExists('products');
    }
};
database/migrations/xxxx_create_carts_table.php:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('carts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')
                ->nullable()
                ->constrained()
                ->cascadeOnDelete();
            $table->string('session_id')->nullable()->index();
            $table->timestamps();
        });

        Schema::create('cart_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('cart_id')
                ->constrained()
                ->cascadeOnDelete();
            $table->foreignId('product_id')
                ->constrained()
                ->cascadeOnDelete();
            $table->integer('quantity')->default(1);
            $table->decimal('price', 10, 2);
            $table->timestamps();

            $table->unique(['cart_id', 'product_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('cart_items');
        Schema::dropIfExists('carts');
    }
};
database/migrations/xxxx_create_orders_table.php:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')
                ->constrained()
                ->cascadeOnDelete();
            $table->string('order_number')->unique();
            $table->enum('status', [
                'pending',
                'processing',
                'completed',
                'cancelled'
            ])->default('pending');
            $table->decimal('subtotal', 10, 2);
            $table->decimal('tax', 10, 2)->default(0);
            $table->decimal('shipping', 10, 2)->default(0);
            $table->decimal('total', 10, 2);
            $table->string('payment_method');
            $table->string('payment_status')->default('pending');
            $table->json('shipping_address');
            $table->json('billing_address');
            $table->text('notes')->nullable();
            $table->timestamps();

            $table->index(['user_id', 'status']);
            $table->index('order_number');
        });

        Schema::create('order_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')
                ->constrained()
                ->cascadeOnDelete();
            $table->foreignId('product_id')
                ->constrained();
            $table->string('product_name'); // لقطة
            $table->integer('quantity');
            $table->decimal('price', 10, 2);
            $table->decimal('subtotal', 10, 2);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('order_items');
        Schema::dropIfExists('orders');
    }
};
database/migrations/xxxx_create_reviews_table.php:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('reviews', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')
                ->constrained()
                ->cascadeOnDelete();
            $table->foreignId('user_id')
                ->constrained()
                ->cascadeOnDelete();
            $table->tinyInteger('rating')->unsigned();
            $table->string('title');
            $table->text('comment')->nullable();
            $table->boolean('is_verified_purchase')->default(false);
            $table->boolean('is_approved')->default(false);
            $table->timestamps();

            $table->index(['product_id', 'is_approved']);
            $table->unique(['product_id', 'user_id']); // مراجعة واحدة لكل مستخدم لكل منتج
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('reviews');
    }
};

إنشاء النماذج

الآن لننشئ نماذج Eloquent مع العلاقات:

app/Models/Category.php:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Category extends Model
{
    protected $fillable = [
        'name',
        'slug',
        'description',
        'image',
        'parent_id',
        'is_active',
    ];

    protected $casts = [
        'is_active' => 'boolean',
    ];

    /**
     * الفئة الأم
     */
    public function parent(): BelongsTo
    {
        return $this->belongsTo(Category::class, 'parent_id');
    }

    /**
     * الفئات الفرعية
     */
    public function children(): HasMany
    {
        return $this->hasMany(Category::class, 'parent_id');
    }

    /**
     * المنتجات في هذه الفئة
     */
    public function products(): HasMany
    {
        return $this->hasMany(Product::class);
    }

    /**
     * نطاق للحصول على الفئات النشطة
     */
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    /**
     * نطاق للحصول على الفئات الجذرية
     */
    public function scopeRoot($query)
    {
        return $query->whereNull('parent_id');
    }
}
app/Models/Product.php:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Product extends Model
{
    protected $fillable = [
        'category_id',
        'name',
        'slug',
        'description',
        'price',
        'sale_price',
        'sku',
        'stock_quantity',
        'is_active',
        'is_featured',
    ];

    protected $casts = [
        'price' => 'decimal:2',
        'sale_price' => 'decimal:2',
        'is_active' => 'boolean',
        'is_featured' => 'boolean',
    ];

    protected $appends = ['current_price', 'in_stock'];

    /**
     * علاقة الفئة
     */
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    /**
     * علاقة الصور
     */
    public function images(): HasMany
    {
        return $this->hasMany(ProductImage::class);
    }

    /**
     * علاقة المراجعات
     */
    public function reviews(): HasMany
    {
        return $this->hasMany(Review::class);
    }

    /**
     * الحصول على السعر الحالي (سعر التخفيض إذا كان متاحًا)
     */
    public function getCurrentPriceAttribute(): float
    {
        return $this->sale_price ?? $this->price;
    }

    /**
     * التحقق من توفر المنتج في المخزون
     */
    public function getInStockAttribute(): bool
    {
        return $this->stock_quantity > 0;
    }

    /**
     * نطاق المنتجات النشطة
     */
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    /**
     * نطاق المنتجات المميزة
     */
    public function scopeFeatured($query)
    {
        return $query->where('is_featured', true);
    }

    /**
     * نطاق المنتجات المتوفرة في المخزون
     */
    public function scopeInStock($query)
    {
        return $query->where('stock_quantity', '>', 0);
    }
}
أفضل ممارسات الترحيل: أنشئ دائمًا فهارس على المفاتيح الخارجية والأعمدة التي تستعلم عنها بشكل متكرر. استخدم أنواع الأعمدة المناسبة (عشري للمال، enum لحقول الحالة). أضف قيود الفرادة عند الحاجة.
تمرين تطبيقي:
  1. قم بتشغيل الترحيلات لإنشاء جميع الجداول
  2. أنشئ النماذج المتبقية (Cart، Order، Review، إلخ)
  3. حدد جميع العلاقات بين النماذج
  4. أضف النطاقات والمحصلات المناسبة للنماذج
  5. أنشئ seeder لملء بيانات الاختبار

الملخص

في الجزء 1، قمنا بـ:

  • التخطيط لنقاط نهاية واجهة برمجة التطبيقات والموارد
  • تصميم مخطط قاعدة بيانات منظم
  • إنشاء ترحيلات بفهارس وقيود مناسبة
  • بناء نماذج Eloquent مع العلاقات
  • إضافة نطاقات ومحصلات للاستعلامات الشائعة

في الجزء 2، سننفذ المصادقة، وننشئ المستودعات والإجراءات، ونبني نقاط نهاية CRUD، ونتعامل مع تحميلات الملفات لصور المنتجات.