إنشاء Dynamic Dependent Dropdown في Laravel باستخدام AJAX و jQuery — دليل شامل

تُعد القوائم المنسدلة المترابطة Dynamic Dependent Dropdowns من العناصر الشائعة في تطبيقات الويب، خصوصاً عندما تعتمد الخيارات الموجودة في قائمة على اختيار المستخدم في قائمة أخرى.

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

في هذا الدليل سنبني هذا النظام باستخدام Laravel وEloquent وBlade وjQuery AJAX، مع تصميم قاعدة البيانات والعلاقات بطريقة تسمح باستخدام الحل نفسه مع الأقسام والأقسام الفرعية أو أي بيانات هرمية مشابهة.

ما هو Dynamic Dependent Dropdown؟

الـ Dependent Dropdown هو حقل <select> تعتمد خياراته على القيمة المختارة في حقل آخر.

لنفرض أن لدينا القائمتين التاليتين:

  • Category: القائمة الرئيسية.
  • Subcategory: القائمة الفرعية.

عندما يختار المستخدم Category معيناً، يرسل JavaScript طلباً إلى Laravel يحتوي على معرّف القسم id. يقوم Laravel بعد ذلك بالبحث عن الأقسام التي يكون parent_id الخاص بها مساوياً لهذا المعرّف، ثم يعيدها بصيغة JSON.

يقوم JavaScript باستقبال النتيجة وتحديث القائمة الثانية مباشرة دون إعادة تحميل الصفحة.

Category
    │
    │ change event
    ▼
JavaScript / AJAX
    │
    │ HTTP Request
    ▼
Laravel Route
    │
    ▼
Controller
    │
    ▼
Eloquent / Database
    │
    ▼
JSON Response
    │
    ▼
JavaScript
    │
    ▼
Subcategory Dropdown

الميزة الأساسية لـ AJAX هي إمكانية التواصل مع الخادم وتحديث جزء من الصفحة دون الحاجة إلى إعادة تحميلها بالكامل.

السيناريو الذي سنقوم ببنائه

سننشئ جدولاً واحداً باسم categories يستطيع تخزين الأقسام الرئيسية والفرعية.

على سبيل المثال:

id name parent_id
1 Electronics NULL
2 Clothes NULL
3 Mobiles 1
4 Laptops 1
5 Men Clothes 2

القيمة NULL تعني أن السجل عبارة عن قسم رئيسي، بينما وجود قيمة داخل parent_id يعني أن السجل يتبع قسماً آخر.

على سبيل المثال، القسم Mobiles يحمل parent_id = 1، وبالتالي فهو تابع للقسم الذي يحمل id = 1 وهو Electronics.

1. إنشاء Model وMigration

نبدأ بإنشاء Model باسم Category مع ملف Migration:

php artisan make:model Category -m

سيقوم Laravel بإنشاء الـ Model بالإضافة إلى Migration خاص بجدول categories.

2. تصميم جدول categories

افتح ملف الـ Migration وأضف الحقول التالية:

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->foreignId('parent_id')
                ->nullable()
                ->constrained('categories')
                ->nullOnDelete();

            $table->timestamps();
        });
    }

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

لماذا استخدمنا foreignId؟

لأن parent_id لا يحتوي على قيمة عشوائية، بل يشير إلى سجل آخر داخل جدول categories نفسه.

وبذلك تصبح لدينا علاقة:

categories.parent_id → categories.id

كما استخدمنا:

->nullable()

للسماح للقسم الرئيسي بعدم امتلاك Parent.

أما:

->nullOnDelete()

فتجعل قيمة parent_id تصبح NULL إذا تم حذف الـ Parent المرتبط بها، بدلاً من ترك مرجع غير صالح داخل قاعدة البيانات.

بعد ذلك قم بتنفيذ:

php artisan migrate

3. إنشاء العلاقة داخل Category Model

لدينا هنا ما يسمى Self-Referencing Relationship، لأن الـ Category مرتبط بسجلات أخرى من نفس الـ Model.

افتح:

app/Models/Category.php

وأضف العلاقات:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'parent_id',
    ];

    public function parent()
    {
        return $this->belongsTo(Category::class, 'parent_id');
    }

    public function subcategories()
    {
        return $this->hasMany(Category::class, 'parent_id');
    }
}

العلاقة parent

public function parent()
{
    return $this->belongsTo(Category::class, 'parent_id');
}

تسمح لنا بمعرفة القسم الأب للسجل الحالي.

العلاقة subcategories

public function subcategories()
{
    return $this->hasMany(Category::class, 'parent_id');
}

تسمح لنا بجلب جميع الأقسام التابعة للقسم الحالي.

على سبيل المثال:

$category->subcategories;

سيعيد جميع الأقسام التي تحمل:

parent_id = $category->id

4. إنشاء Controller

نقوم الآن بإنشاء Controller:

php artisan make:controller CategoryController

ثم نستورد الـ Model والـ Request:

use App\Models\Category;
use Illuminate\Http\Request;

5. إنشاء Routes

افتح:

routes/web.php

ثم أضف:

use App\Http\Controllers\CategoryController;

Route::get('/categories', [CategoryController::class, 'index'])
    ->name('categories.index');

Route::post('/categories/subcategories', [CategoryController::class, 'subcategories'])
    ->name('categories.subcategories');

لدينا هنا Routeان:

  • categories.index لعرض الصفحة والأقسام الرئيسية.
  • categories.subcategories لاستقبال طلب AJAX وإرجاع الأقسام الفرعية.

6. جلب الأقسام الرئيسية

داخل CategoryController نضيف:

public function index()
{
    $categories = Category::query()
        ->whereNull('parent_id')
        ->orderBy('name')
        ->get();

    return view('category', compact('categories'));
}

لاحظ استخدام:

whereNull('parent_id')

وبذلك نقوم بجلب الأقسام الرئيسية فقط.

كما استخدمنا orderBy('name') لترتيب النتائج أبجدياً قبل عرضها.

7. إنشاء واجهة Blade

أنشئ الملف:

resources/views/category.blade.php

وأضف الهيكل التالي:

<!DOCTYPE html>
<html lang="ar" dir="rtl">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>Dynamic Dependent Dropdown - Laravel</title>

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>

<body>

    <main>

        <h1>اختيار القسم</h1>

        <form>

            <div>
                <label for="category">القسم الرئيسي</label>

                <select name="category_id" id="category">

                    <option value="">اختر القسم الرئيسي</option>

                    @foreach ($categories as $category)
                        <option value="{{ $category->id }}">
                            {{ $category->name }}
                        </option>
                    @endforeach

                </select>
            </div>

            <div>
                <label for="subcategory">القسم الفرعي</label>

                <select name="subcategory_id" id="subcategory" disabled>
                    <option value="">اختر القسم الرئيسي أولاً</option>
                </select>
            </div>

        </form>

    </main>

</body>
</html>

لاحظ أن القائمة الثانية تبدأ بالحالة:

disabled

لأنه لا يمكن اختيار Subcategory قبل اختيار Category.

8. لماذا نحتاج إلى CSRF Token؟

بما أننا نستخدم Route من نوع POST داخل web.php، فإن Laravel يطبق حماية CSRF على الطلب.

لذلك أضفنا داخل <head>:

<meta name="csrf-token" content="{{ csrf_token() }}">

وسيقرأ JavaScript هذه القيمة ويرسلها داخل Header الطلب.

إذا لم يتم إرسال Token صحيح، فمن المتوقع أن يرفض Laravel الطلب بدلاً من تمريره إلى Controller.

9. إنشاء AJAX Request

قبل إغلاق </body> أضف:

<script>
$(document).ready(function () {

    $.ajaxSetup({
        headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
        }
    });

    $('#category').on('change', function () {

        const categoryId = $(this).val();
        const subcategory = $('#subcategory');

        subcategory
            .prop('disabled', true)
            .html('<option value="">جاري تحميل البيانات...</option>');

        if (!categoryId) {
            subcategory.html(
                '<option value="">اختر القسم الرئيسي أولاً</option>'
            );

            return;
        }

        $.ajax({
            url: "{{ route('categories.subcategories') }}",
            type: "POST",
            dataType: "json",

            data: {
                category_id: categoryId
            },

            success: function (response) {

                subcategory.empty();

                if (response.data.length === 0) {

                    subcategory.html(
                        '<option value="">لا توجد أقسام فرعية</option>'
                    );

                    return;
                }

                subcategory.append(
                    '<option value="">اختر القسم الفرعي</option>'
                );

                $.each(response.data, function (index, item) {

                    subcategory.append(
                        $('<option>', {
                            value: item.id,
                            text: item.name
                        })
                    );

                });

                subcategory.prop('disabled', false);
            },

            error: function (xhr) {

                console.error(xhr.responseText);

                subcategory.html(
                    '<option value="">حدث خطأ أثناء تحميل البيانات</option>'
                );
            }
        });
    });
});
</script>

10. شرح AJAX بالتفصيل

الاستماع إلى تغيير Category

$('#category').on('change', function () {

يتم تنفيذ الدالة بمجرد أن يغيّر المستخدم القيمة الموجودة داخل القائمة الرئيسية.

الحصول على ID

const categoryId = $(this).val();

إذا اختار المستخدم Category يحمل ID بقيمة 5، تصبح:

categoryId = 5

إرسال الطلب

$.ajax({
    url: "{{ route('categories.subcategories') }}",
    type: "POST",
    dataType: "json",

    data: {
        category_id: categoryId
    }
});

سيصل إلى Laravel متغير باسم:

category_id

وقيمته هي ID القسم الذي اختاره المستخدم.

11. معالجة طلب AJAX داخل Laravel

نضيف داخل CategoryController:

public function subcategories(Request $request)
{
    $validated = $request->validate([
        'category_id' => ['required', 'integer', 'exists:categories,id'],
    ]);

    $subcategories = Category::query()
        ->where('parent_id', $validated['category_id'])
        ->orderBy('name')
        ->get(['id', 'name']);

    return response()->json([
        'data' => $subcategories,
    ]);
}

هذا الجزء مهم لأننا لا ينبغي أن نثق مباشرة بالقيمة القادمة من المتصفح.

نقوم أولاً بالتحقق منها:

'category_id' => [
    'required',
    'integer',
    'exists:categories,id'
]

أي أن القيمة يجب أن:

  • تكون موجودة.
  • تكون رقماً صحيحاً.
  • تشير إلى Category موجود فعلياً في قاعدة البيانات.

12. جلب الأقسام الفرعية

بعد Validation ننفذ:

$subcategories = Category::query()
    ->where('parent_id', $validated['category_id'])
    ->orderBy('name')
    ->get(['id', 'name']);

إذا كان المستخدم قد اختار Category رقم 5، يصبح الاستعلام منطقياً:

SELECT id, name
FROM categories
WHERE parent_id = 5
ORDER BY name;

استخدمنا:

get(['id', 'name'])

بدلاً من إرجاع جميع أعمدة الجدول، لأن الواجهة تحتاج فقط إلى id وname.

هذا يقلل حجم استجابة JSON، وهو أمر يصبح أكثر أهمية كلما زاد حجم البيانات وعدد الطلبات.

13. شكل JSON Response

نعيد البيانات:

return response()->json([
    'data' => $subcategories,
]);

وقد تكون النتيجة مثلاً:

{
    "data": [
        {
            "id": 10,
            "name": "Laptops"
        },
        {
            "id": 11,
            "name": "Mobiles"
        },
        {
            "id": 12,
            "name": "Tablets"
        }
    ]
}

وبذلك يصبح التعامل معها في JavaScript بسيطاً:

response.data

14. إضافة النتائج إلى Dropdown

بعد نجاح الطلب نقوم أولاً بتفريغ القائمة:

subcategory.empty();

ثم نمر على النتائج:

$.each(response.data, function (index, item) {

    subcategory.append(
        $('<option>', {
            value: item.id,
            text: item.name
        })
    );

});

لكل عنصر يتم إنشاء:

<option value="10">Laptops</option>

ثم تتم إضافته إلى القائمة الثانية.

15. التعامل مع حالة عدم وجود نتائج

من الأخطاء الشائعة افتراض أن كل Category يحتوي بالضرورة على Subcategories.

لذلك نتحقق من:

if (response.data.length === 0) {

    subcategory.html(
        '<option value="">لا توجد أقسام فرعية</option>'
    );

    return;
}

بهذه الطريقة يحصل المستخدم على رسالة واضحة بدلاً من قائمة فارغة.

16. التعامل مع الأخطاء

أي اتصال HTTP يمكن أن يفشل، لذلك يجب ألا نعتمد على success فقط.

error: function (xhr) {

    console.error(xhr.responseText);

    subcategory.html(
        '<option value="">حدث خطأ أثناء تحميل البيانات</option>'
    );
}

قد يحدث الخطأ بسبب:

  • خطأ في Route.
  • فشل Validation.
  • انتهاء Session.
  • CSRF Token غير صالح.
  • خطأ داخلي في Laravel.
  • مشكلة في الاتصال بالشبكة.
  • مشكلة في قاعدة البيانات.

17. لماذا لا نضع صورة Loading داخل select؟

قد نجد بعض الأمثلة القديمة تستخدم شيئاً مثل:

$('#subcategory').html('<img src="loading.gif">');

لكن عنصر <select> مصمم لاحتواء عناصر <option> وليس عناصر HTML عشوائية مثل الصور.

لذلك نستخدم حالة تحميل واضحة:

<option value="">جاري تحميل البيانات...</option>

ونقوم بتعطيل القائمة أثناء تنفيذ الطلب.

18. Controller كاملاً

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use Illuminate\Http\Request;

class CategoryController extends Controller
{
    public function index()
    {
        $categories = Category::query()
            ->whereNull('parent_id')
            ->orderBy('name')
            ->get();

        return view('category', compact('categories'));
    }

    public function subcategories(Request $request)
    {
        $validated = $request->validate([
            'category_id' => [
                'required',
                'integer',
                'exists:categories,id',
            ],
        ]);

        $subcategories = Category::query()
            ->where('parent_id', $validated['category_id'])
            ->orderBy('name')
            ->get(['id', 'name']);

        return response()->json([
            'data' => $subcategories,
        ]);
    }
}

19. Routes كاملة

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CategoryController;

Route::get('/categories', [CategoryController::class, 'index'])
    ->name('categories.index');

Route::post('/categories/subcategories', [CategoryController::class, 'subcategories'])
    ->name('categories.subcategories');

20. Migration كاملة

<?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->foreignId('parent_id')
                ->nullable()
                ->constrained('categories')
                ->nullOnDelete();

            $table->timestamps();
        });
    }

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

21. Category Model كاملاً

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'parent_id',
    ];

    public function parent()
    {
        return $this->belongsTo(Category::class, 'parent_id');
    }

    public function subcategories()
    {
        return $this->hasMany(Category::class, 'parent_id');
    }
}

22. ملف Blade كاملاً

<!DOCTYPE html>
<html lang="ar" dir="rtl">

<head>

    <meta charset="UTF-8">

    <meta name="viewport"
          content="width=device-width, initial-scale=1.0">

    <meta name="csrf-token"
          content="{{ csrf_token() }}">

    <title>Dynamic Dependent Dropdown باستخدام Laravel و AJAX</title>

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

</head>

<body>

<main>

    <h1>Dynamic Dependent Dropdown</h1>

    <form>

        <div>

            <label for="category">
                القسم الرئيسي
            </label>

            <select
                name="category_id"
                id="category"
            >

                <option value="">
                    اختر القسم الرئيسي
                </option>

                @foreach ($categories as $category)

                    <option value="{{ $category->id }}">
                        {{ $category->name }}
                    </option>

                @endforeach

            </select>

        </div>


        <div>

            <label for="subcategory">
                القسم الفرعي
            </label>

            <select
                name="subcategory_id"
                id="subcategory"
                disabled
            >

                <option value="">
                    اختر القسم الرئيسي أولاً
                </option>

            </select>

        </div>

    </form>

</main>


<script>

$(document).ready(function () {

    $.ajaxSetup({

        headers: {

            'X-CSRF-TOKEN':
                $('meta[name="csrf-token"]').attr('content')

        }

    });


    $('#category').on('change', function () {

        const categoryId = $(this).val();

        const subcategory = $('#subcategory');


        subcategory
            .prop('disabled', true)
            .html(
                '<option value="">جاري تحميل البيانات...</option>'
            );


        if (!categoryId) {

            subcategory.html(
                '<option value="">اختر القسم الرئيسي أولاً</option>'
            );

            return;

        }


        $.ajax({

            url: "{{ route('categories.subcategories') }}",

            type: "POST",

            dataType: "json",

            data: {

                category_id: categoryId

            },


            success: function (response) {

                subcategory.empty();


                if (response.data.length === 0) {

                    subcategory.html(
                        '<option value="">لا توجد أقسام فرعية</option>'
                    );

                    return;

                }


                subcategory.append(
                    '<option value="">اختر القسم الفرعي</option>'
                );


                $.each(response.data, function (index, item) {

                    subcategory.append(

                        $('<option>', {

                            value: item.id,

                            text: item.name

                        })

                    );

                });


                subcategory.prop('disabled', false);

            },


            error: function (xhr) {

                console.error(xhr.responseText);


                subcategory.html(
                    '<option value="">حدث خطأ أثناء تحميل البيانات</option>'
                );

            }

        });

    });

});

</script>

</body>
</html>

23. كيف يعمل النظام من البداية إلى النهاية؟

  1. يفتح المستخدم صفحة /categories.
  2. يقوم Laravel بجلب الأقسام التي يكون parent_id الخاص بها NULL.
  3. يتم إرسال هذه البيانات إلى Blade.
  4. يقوم Blade بعرض الأقسام الرئيسية داخل أول <select>.
  5. يختار المستخدم أحد الأقسام.
  6. يتم تشغيل حدث change.
  7. يقرأ JavaScript قيمة category_id.
  8. يتم تعطيل القائمة الثانية وإظهار حالة التحميل.
  9. يرسل AJAX طلب POST إلى Laravel.
  10. يتحقق Laravel من صحة category_id.
  11. يبحث Eloquent عن السجلات التي تحمل هذا الرقم داخل parent_id.
  12. يعيد Laravel النتائج بصيغة JSON.
  13. يستقبل JavaScript الاستجابة.
  14. يتم إنشاء عناصر <option> الجديدة.
  15. يتم تفعيل القائمة الثانية ليتمكن المستخدم من اختيار Subcategory.

24. تطبيق المثال على الدول والمدن

نفس المبدأ يمكن تطبيقه على الدول والمدن.

id name parent_id
1 فلسطين NULL
2 الأردن NULL
3 الخليل 1
4 رام الله 1
5 نابلس 1
6 عمّان 2

عند اختيار:

فلسطين → id = 1

يصبح الاستعلام:

WHERE parent_id = 1

فتظهر:

الخليل
رام الله
نابلس

وعند اختيار الأردن:

WHERE parent_id = 2

فتظهر المدن المرتبطة بها.

25. هل يجب استخدام جدول واحد دائماً؟

لا. استخدام categories.parent_id ممتاز عندما تكون البيانات من النوع نفسه، مثل:

Category
└── Subcategory
    └── Subcategory

لكن إذا كانت الكيانات مختلفة منطقياً، فقد يكون فصل الجداول أفضل.

مثلاً:

countries
cities

ويحتوي جدول cities على:

country_id

فتصبح العلاقة:

Country hasMany Cities
City belongsTo Country

اختيار التصميم يعتمد على طبيعة البيانات وليس فقط على شكل الـ Dropdown في الواجهة.

26. تحسين الأداء عند وجود عدد كبير من السجلات

في التطبيقات الصغيرة لن يكون الفرق ملحوظاً، لكن عند وجود آلاف أو ملايين السجلات يصبح تصميم قاعدة البيانات مهماً.

حقل parent_id المستخدم باستمرار في:

WHERE parent_id = ?

يجب أن يكون مفهرساً. إنشاء Foreign Key باستخدام Laravel يؤدي كذلك إلى إنشاء الفهرسة المطلوبة لهذا النوع من العلاقات في قواعد البيانات المعتادة.

كذلك من الأفضل عدم إرجاع بيانات غير مطلوبة.

بدلاً من:

Category::where('parent_id', $id)->get();

نستخدم:

Category::where('parent_id', $id)
    ->get(['id', 'name']);

27. منع الطلبات غير الضرورية

إذا أعاد المستخدم اختيار القيمة الافتراضية:

<option value="">اختر القسم الرئيسي</option>

لا يوجد سبب لإرسال AJAX Request إلى السيرفر.

لهذا نستخدم:

if (!categoryId) {

    subcategory.html(
        '<option value="">اختر القسم الرئيسي أولاً</option>'
    );

    return;
}

وبذلك ينتهي التنفيذ قبل إرسال أي Request.

28. الجانب الأمني

استخدام AJAX لا يعني أن البيانات القادمة من المستخدم موثوقة.

يمكن لأي مستخدم تعديل الطلب يدوياً وإرسال قيمة مختلفة عن الموجودة في Dropdown، لذلك يجب تنفيذ Validation دائماً على السيرفر.

$request->validate([
    'category_id' => [
        'required',
        'integer',
        'exists:categories,id',
    ],
]);

كما يجب عدم الاعتماد على JavaScript للتحقق الأمني، لأن JavaScript يعمل على جهاز المستخدم ويمكن تعديله أو تجاوزه.

29. أخطاء شائعة عند إنشاء Dependent Dropdown

استخدام parent_id = 0

يمكن أن يعمل تقنياً في بعض التصاميم، لكنه أقل وضوحاً من استخدام NULL، كما أنه يصعّب إنشاء علاقة Foreign Key صحيحة إذا لم يوجد سجل ID=0.

عدم استخدام Validation

لا تستخدم قيمة category_id القادمة من المتصفح مباشرة دون التحقق منها.

إرجاع Model كامل دون حاجة

إذا كان Dropdown يحتاج id وname فقط، فلا داعي لإرسال timestamps أو أعمدة أخرى.

عدم معالجة الأخطاء

وجود success دون error يجعل الواجهة غير واضحة للمستخدم عند فشل الاتصال.

عدم معالجة Empty State

يجب إخبار المستخدم عندما لا يحتوي القسم على أي عناصر فرعية بدلاً من ترك القائمة فارغة.

عدم تعطيل القائمة أثناء التحميل

تعطيل الـ Subcategory أثناء انتظار الاستجابة يمنع المستخدم من التفاعل مع بيانات قديمة.

30. استخدام GET بدلاً من POST

في هذا المثال استخدمنا POST لتوضيح التعامل مع AJAX وCSRF داخل Laravel، لكن عملية جلب الأقسام الفرعية هي في الأصل عملية قراءة للبيانات وليست تعديلها.

لذلك يمكن تصميم Endpoint باستخدام GET، مثل:

GET /categories/{category}/subcategories

مثلاً:

Route::get(
    '/categories/{category}/subcategories',
    [CategoryController::class, 'subcategories']
)->name('categories.subcategories');

وهذا التصميم قد يكون أكثر تعبيراً عن طبيعة العملية في RESTful APIs، لأننا نقوم بجلب Resource وليس إنشائه أو تعديله.

عند استخدام GET لجلب البيانات بهذه الطريقة لن تحتاج إلى إرسال CSRF Token لهذا الطلب، لأن Laravel يطبق CSRF على الطلبات التي تغيّر الحالة مثل POST وPUT وPATCH وDELETE ضمن مجموعة web middleware، وليس على GET.

31. توسيع النظام إلى أكثر من مستويين

الفكرة ليست محدودة بقائمتين فقط.

يمكن بناء:

Country
   ↓
State
   ↓
City
   ↓
Area

أو:

Category
   ↓
Subcategory
   ↓
Product

أو:

Car Manufacturer
   ↓
Model
   ↓
Year
   ↓
Engine

في كل مستوى يتم أخذ قيمة القائمة السابقة، إرسال Request إلى السيرفر، جلب البيانات المرتبطة، ثم تحديث القائمة التالية.

الخلاصة

يعتمد Dynamic Dependent Dropdown على فكرة بسيطة: لا نقوم بتحميل جميع الخيارات منذ البداية، بل نجلب البيانات المطلوبة عندما تصبح ضرورية بناءً على اختيار المستخدم.

في Laravel يمكن تنفيذ ذلك بكفاءة من خلال الجمع بين:

  • Migration لبناء قاعدة البيانات والعلاقات.
  • Eloquent للتعامل مع البيانات.
  • Self-Referencing Relationships عند التعامل مع البيانات الهرمية.
  • Controller لمعالجة الطلبات.
  • Validation للتحقق من البيانات القادمة من المستخدم.
  • Blade لبناء واجهة النموذج.
  • jQuery AJAX لإرسال الطلبات دون إعادة تحميل الصفحة.
  • JSON لنقل البيانات بين Laravel والواجهة الأمامية.
  • CSRF Protection لحماية طلبات POST داخل تطبيقات Laravel.

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