كيفية بناء واجهة GraphQL API باستخدام Laravel خطوة بخطوة
في هذا الدليل العملي سنتعلم كيفية إنشاء واجهة GraphQL API باستخدام PHP وLaravel بأسلوب واضح ومناسب للتطبيق الفعلي. قد تبدو مفاهيم مثل Docker وGraphQL معقدة في البداية، لكن عند تقسيمها إلى خطوات صغيرة تصبح أسهل بكثير مما يتوقعه معظم المطورين.
سننفذ مشروعاً تجريبياً بسيطاً لإدارة المهام داخل لعبة، بحيث يحتوي التطبيق على نموذجين رئيسيين: Quests وCategories. وفي نهاية الشرح ستكون لديك واجهة CRUD كاملة عبر GraphQL لكل نموذج.

المتطلبات الأساسية قبل البدء
قبل تنفيذ المشروع، تأكد من توفر الأدوات التالية على جهازك:
PHP 7+Composer 2.0Docker 20.10.6أو أي إصدار قريبDocker-Compose 1.29.1أو أي إصدار قريب
ويُفضّل أيضاً أن تكون لديك معرفة أساسية بالمفاهيم التالية:
- أساسيات
LaravelمثلEloquentوMigrationsوMVCوRoutes - قواعد لغة
PHPوالبرمجة الكائنيةOOP - فهم نظري لمفهوم
GraphQL
ما الذي سنبنيه في هذا المشروع؟
سنطوّر تطبيقاً صغيراً يدور حول عالم الألعاب، ويتضمن كيانين فقط:
Category: لتصنيف المهامQuest: لتمثيل المهمة نفسها
الهدف هو بناء واجهة GraphQL API تدعم:
- جلب عنصر واحد
- جلب قائمة عناصر
- إضافة سجل جديد
- تحديث سجل موجود
- حذف سجل
تهيئة مشروع Laravel
ابدأ بإنشاء مشروع Laravel جديد عبر الأمر التالي:
composer create-project laravel/laravel quest_journal
سينشئ هذا الأمر مجلداً جديداً باسم quest_journal. بعد ذلك ننتقل إلى إعداد Laravel Sail لتشغيل المشروع عبر الحاويات.
# Move into the project
cd quest_journal
# Install and configure laravel sail
php artisan sail:install
عند سؤالك عن الخدمات التي تريد تثبيتها، يمكنك الضغط على Enter لاختيار MySQL فقط. إذا سارت العملية بشكل صحيح فستجد ملف docker-compose.yml داخل المشروع.
شغّل الحاويات باستخدام:
# Run the containers
./vendor/bin/sail up -d
# Check if the containers are running
docker ps
ولتسهيل الاستخدام اليومي، يمكنك إنشاء اسم مختصر للأمر sail داخل ملف bashrc أو zshrc:
# in ~/.zshrc or ~/.bashrc
alias sail='bash vendor/bin/sail'
بعد تشغيل المشروع، افتح localhost في المتصفح. من المفترض أن تظهر الصفحة الافتراضية الخاصة بـLaravel.

تثبيت الحزم اللازمة لـ GraphQL
نحتاج إلى تثبيت حزمتين أساسيتين: الأولى لتحسين تجربة التطوير، والثانية لبناء واجهة GraphQL.
# IDE helper for laravel, always useful to have.
sail composer require --dev barryvdh/laravel-ide-helper
# GraphQL library which we are going to use
sail composer require rebing/graphql-laravel
بعد التثبيت، انشر إعدادات الحزمة إلى ملفات المشروع:
sail artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider"
سيؤدي ذلك إلى إنشاء ملف إعدادات داخل المسار config/graphql.php، وهو الملف الذي سنسجل فيه الأنواع والاستعلامات وعمليات التعديل لاحقاً.
إنشاء الجداول والنماذج
إنشاء نموذج التصنيفات Category
أنشئ النموذج مع ملف الترحيل الخاص به:
sail artisan make:model -m Category
سيحتوي جدول التصنيفات على الحقول التالية:
idtitlecreated_atupdated_at
ملف الترحيل الخاص بجدول التصنيفات:
<?php
// database/migrations/yyyy_mm_dd_hhMMss_create_categories_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCategoriesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->text('title');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('categories');
}
}
بعد ذلك نضبط النموذج نفسه، بحيث:
- نجعل الحقل
titleقابلاً للتعبئة عبر المصفوفة$fillable - نعرّف العلاقة مع نموذج
Quest
<?php
// App\Models\Category
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $fillable = ['title'];
public function quests()
{
return $this->hasMany(Quest::class);
}
}
إنشاء نموذج المهام Quest
الآن أنشئ نموذج المهمة مع ملف الترحيل:
sail artisan make:model -m Quest
سيحتوي جدول المهام على:
idtitledescriptionrewardcategory_idcreated_atupdated_at
<?php
// database/migrations/yyyy_mm_dd_hhMMss_create_quests_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateQuestsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('quests', function (Blueprint $table) {
$table->id();
$table->text('title');
$table->text('description');
$table->integer('reward');
$table->foreignId('category_id')->constrained();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('quests');
}
}
لاحظ أننا استخدمنا foreignId() مع الحقل category_id، وهذا يتيح لـLaravel إنشاء علاقة مفتاح أجنبي تلقائياً بين جدولي categories وquests.
أما النموذج Quest فيكون كالتالي:
<?php
// App\Models\Quest
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Quest extends Model
{
use HasFactory;
protected $fillable = [
'title',
'category_id',
'description',
'reward'
];
public function category()
{
return $this->belongsTo(Category::class);
}
}
بعد تجهيز ملفات الترحيل والنماذج، طبّق التغييرات على قاعدة البيانات:
# Apply migrations
sail artisan migrate
توليد بيانات تجريبية داخل قاعدة البيانات
بدلاً من إدخال البيانات يدوياً، سنستخدم Factories لإنشاء سجلات اختبارية بسرعة.
# Create a factory class for quest model
sail artisan make:factory QuestFactory --model=Quest
# Create a factory class for category model
sail artisan make:factory CategoryFactory --model=Category
ملف QuestFactory
سننشئ بيانات المهام، مع اختيار category_id عشوائياً من التصنيفات الموجودة:
<?php
// database/factories/QuestFactory.php
namespace Database\Factories;
use App\Models\Category;
use App\Models\Quest;
use Illuminate\Database\Eloquent\Factories\Factory;
class QuestFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Quest::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
$categoryIDs = Category::all()->pluck('id')->toArray();
return [
'title' => $this->faker->title(),
'description' => $this->faker->text(),
'reward' => $this->faker->numberBetween(1, 100),
'category_id' => $this->faker->randomElement($categoryIDs)
];
}
}
ملف CategoryFactory
هذا الملف أبسط، لأننا نحتاج فقط إلى عنوان للتصنيف:
<?php
// database/factories/CategoryFactory.php
namespace Database\Factories;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
class CategoryFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Category::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'title' => $this->faker->title()
];
}
}
تعبئة قاعدة البيانات
بدلاً من إنشاء ملفات Seeder متعددة، يمكننا استدعاء المصانع مباشرة من الملف DatabaseSeeder.php:
<?php
// database/seeders/DatabaseSeeder.php
namespace Database\Seeders;
use App\Models\Category;
use App\Models\Quest;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
Category::factory(10)->create();
Quest::factory(10)->create();
}
}
ثم نفذ الأمر التالي:
sail artisan db:seed
تنظيم بنية ملفات GraphQL داخل المشروع
أنشئ مجلداً جديداً باسم GraphQL داخل المجلد app، ثم أضف بداخله ثلاثة مجلدات:
MutationsQueriesTypes

هذا التقسيم مهم لفهم طريقة عمل GraphQL:
Types: تمثل الكيانات التي يمكن إرجاعها من الواجهة البرمجيةQueries: مسؤولة عن جلب البياناتMutations: مسؤولة عن عمليات الإنشاء والتعديل والحذف
تعريف الأنواع Types في GraphQL
سننشئ نوعين:
CategoryTypeQuestType
تعتمد هذه الأنواع على الحزمة rebing/graphql-laravel، وترث من الصنف Rebing\GraphQL\Support\Type.
ملف CategoryType
<?php
// app/graphql/types/CategoryType
namespace App\GraphQL\Types;
use App\Models\Category;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;
class CategoryType extends GraphQLType
{
protected $attributes = [
'name' => 'Category',
'description' => 'Collection of categories',
'model' => Category::class
];
public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
'description' => 'ID of quest'
],
'title' => [
'type' => Type::nonNull(Type::string()),
'description' => 'Title of the quest'
],
'quests' => [
'type' => Type::listOf(GraphQL::type('Quest')),
'description' => 'List of quests'
]
];
}
}
يتكون هذا النوع من جزأين أساسيين:
attributes: تحتوي معلومات تعريفية عن النوع وربطه بالنموذجfields(): تحدد الحقول التي يستطيع العميل طلبها
ملف QuestType
<?php
// app/graphql/types/QuestType
namespace App\GraphQL\Types;
use App\Models\Quest;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;
class QuestType extends GraphQLType
{
protected $attributes = [
'name' => 'Quest',
'description' => 'Collection of quests with their respective category',
'model' => Quest::class
];
public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
'description' => 'ID of quest'
],
'title' => [
'type' => Type::nonNull(Type::string()),
'description' => 'Title of the quest'
],
'description' => [
'type' => Type::nonNull(Type::string()),
'description' => 'Description of quest'
],
'reward' => [
'type' => Type::nonNull(Type::int()),
'description' => 'Quest reward'
],
'category' => [
'type' => GraphQL::type('Category'),
'description' => 'The category of the quest'
]
];
}
}
إنشاء الاستعلامات Queries
لكل نموذج سننشئ استعلامين:
- استعلام لإرجاع عنصر واحد
- استعلام لإرجاع قائمة عناصر
ولتنظيم الكود، أنشئ داخل مجلد Queries مجلدين فرعيين:
CategoryQuest
ثم أضف الملفات التالية:
QuestQueryQuestsQueryCategoryQueryCategoriesQuery

استعلام عنصر واحد من نوع Quest
<?php
// app/graphql/queries/quest/QuestQuery
namespace App\GraphQL\Queries\Quest;
use App\Models\Quest;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class QuestQuery extends Query
{
protected $attributes = [
'name' => 'quest',
];
public function type(): Type
{
return GraphQL::type('Quest');
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::int(),
'rules' => ['required']
]
];
}
public function resolve($root, $args)
{
return Quest::findOrFail($args['id']);
}
}
هذا النمط سيتكرر مع بقية الاستعلامات، حيث:
type()تحدد نوع البيانات المرجعةargs()تحدد المدخلات المطلوبةresolve()تنفذ المنطق الفعلي لجلب البيانات
استعلامات القوائم والعناصر الأخرى
<?php
// app/graphql/queries/quest/QuestsQuery
namespace App\GraphQL\Queries\Quest;
use App\Models\Quest;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class QuestsQuery extends Query
{
protected $attributes = [
'name' => 'quests',
];
public function type(): Type
{
return Type::listOf(GraphQL::type('Quest'));
}
public function resolve($root, $args)
{
return Quest::all();
}
}
<?php
// app/graphql/queries/category/CategoryQuery
namespace App\GraphQL\Queries\Category;
use App\Models\Category;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class CategoryQuery extends Query
{
protected $attributes = [
'name' => 'category',
];
public function type(): Type
{
return GraphQL::type('Category');
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::int(),
'rules' => ['required']
]
];
}
public function resolve($root, $args)
{
return Category::findOrFail($args['id']);
}
}
<?php
// app/graphql/queries/category/CategoriesQuery
namespace App\GraphQL\Queries\Category;
use App\Models\Category;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class CategoriesQuery extends Query
{
protected $attributes = [
'name' => 'categories',
];
public function type(): Type
{
return Type::listOf(GraphQL::type('Category'));
}
public function resolve($root, $args)
{
return Category::all();
}
}
إنشاء عمليات Mutations
تمثل Mutations الأوامر التي تُغيّر البيانات. لكل نموذج سنحتاج إلى ثلاث عمليات:
- إنشاء
- تحديث
- حذف
أنشئ داخل مجلد Mutations مجلدين فرعيين:
CategoryQuest
ثم أضف هذه الملفات:
CreateCategoryMutationDeleteCategoryMutationUpdateCategoryMutationCreateQuestMutationDeleteQuestMutationUpdateQuestMutation

إنشاء تصنيف جديد
<?php
// app/graphql/mutations/category/CreateCategoryMutation
namespace App\GraphQL\Mutations\Category;
use App\Models\Category;
use Rebing\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
class CreateCategoryMutation extends Mutation
{
protected $attributes = [
'name' => 'createCategory',
'description' => 'Creates a category'
];
public function type(): Type
{
return GraphQL::type('Category');
}
public function args(): array
{
return [
'title' => [
'name' => 'title',
'type' => Type::nonNull(Type::string()),
],
];
}
public function resolve($root, $args)
{
$category = new Category();
$category->fill($args);
$category->save();
return $category;
}
}
حذف وتحديث التصنيف
<?php
// app/graphql/mutations/category/DeleteCategoryMutation
namespace App\GraphQL\Mutations\Category;
use App\Models\Category;
use Rebing\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\Type;
class DeleteCategoryMutation extends Mutation
{
protected $attributes = [
'name' => 'deleteCategory',
'description' => 'deletes a category'
];
public function type(): Type
{
return Type::boolean();
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::int(),
'rules' => ['required']
]
];
}
public function resolve($root, $args)
{
$category = Category::findOrFail($args['id']);
return $category->delete() ? true : false;
}
}
<?php
// app/graphql/mutations/category/UpdateCategoryMutation
namespace App\GraphQL\Mutations\Category;
use App\Models\Category;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Mutation;
class UpdateCategoryMutation extends Mutation
{
protected $attributes = [
'name' => 'updateCategory',
'description' => 'Updates a category'
];
public function type(): Type
{
return GraphQL::type('Category');
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
],
'title' => [
'name' => 'title',
'type' => Type::nonNull(Type::string()),
],
];
}
public function resolve($root, $args)
{
$category = Category::findOrFail($args['id']);
$category->fill($args);
$category->save();
return $category;
}
}
إنشاء مهمة جديدة
<?php
// app/graphql/mutations/quest/CreateQuestMutation
namespace App\GraphQL\Mutations\Quest;
use App\Models\Quest;
use Rebing\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
class CreateQuestMutation extends Mutation
{
protected $attributes = [
'name' => 'createQuest',
'description' => 'Creates a quest'
];
public function type(): Type
{
return GraphQL::type('Quest');
}
public function args(): array
{
return [
'title' => [
'name' => 'title',
'type' => Type::nonNull(Type::string()),
],
'description' => [
'name' => 'description',
'type' => Type::nonNull(Type::string()),
],
'reward' => [
'name' => 'reward',
'type' => Type::nonNull(Type::int()),
],
'category_id' => [
'name' => 'category_id',
'type' => Type::nonNull(Type::int()),
'rules' => ['exists:categories,id']
]
];
}
public function resolve($root, $args)
{
$quest = new Quest();
$quest->fill($args);
$quest->save();
return $quest;
}
}
حذف وتحديث المهمة
<?php
// app/graphql/mutations/quest/DeleteQuestMutation
namespace App\GraphQL\Mutations\Quest;
use App\Models\Quest;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Mutation;
class DeleteQuestMutation extends Mutation
{
protected $attributes = [
'name' => 'deleteQuest',
'description' => 'Deletes a quest'
];
public function type(): Type
{
return Type::boolean();
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
'rules' => ['exists:quests']
]
];
}
public function resolve($root, $args)
{
$category = Quest::findOrFail($args['id']);
return $category->delete() ? true : false;
}
}
<?php
// app/graphql/mutations/quest/UpdateQuestMutation
namespace App\GraphQL\Mutations\Quest;
use App\Models\Quest;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Mutation;
class UpdateQuestMutation extends Mutation
{
protected $attributes = [
'name' => 'updateQuest',
'description' => 'Updates a quest'
];
public function type(): Type
{
return GraphQL::type('Quest');
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
],
'title' => [
'name' => 'title',
'type' => Type::nonNull(Type::string()),
],
'description' => [
'name' => 'description',
'type' => Type::nonNull(Type::string()),
],
'reward' => [
'name' => 'reward',
'type' => Type::nonNull(Type::int()),
],
'category_id' => [
'name' => 'category_id',
'type' => Type::nonNull(Type::int()),
'rules' => ['exists:categories,id']
]
];
}
public function resolve($root, $args)
{
$quest = Quest::findOrFail($args['id']);
$quest->fill($args);
$quest->save();
return $quest;
}
}
تسجيل الأنواع والاستعلامات والعمليات في الإعدادات
بعد الانتهاء من بناء ملفات Types وQueries وMutations، نربطها جميعاً داخل ملف config/graphql.php:
<?php
return [
// ... some code
'schemas' => [
'default' => [
'query' => [
'quest' => \App\GraphQL\Queries\Quest\QuestQuery::class,
'quests' => \App\GraphQL\Queries\Quest\QuestsQuery::class,
'category' => \App\GraphQL\Queries\Category\CategoryQuery::class,
'categories' => \App\GraphQL\Queries\Category\CategoriesQuery::class,
],
'mutation' => [
'createQuest' => \App\GraphQL\Mutations\Quest\CreateQuestMutation::class,
'updateQuest' => \App\GraphQL\Mutations\Quest\UpdateQuestMutation::class,
'deleteQuest' => \App\GraphQL\Mutations\Quest\DeleteQuestMutation::class,
'createCategory' => \App\GraphQL\Mutations\Category\CreateCategoryMutation::class,
'updateCategory' => \App\GraphQL\Mutations\Category\UpdateCategoryMutation::class,
'deleteCategory' => \App\GraphQL\Mutations\Category\DeleteCategoryMutation::class,
],
'middleware' => [],
'method' => ['get', 'post'],
],
],
'types' => [
'Quest' => \App\GraphQL\Types\QuestType::class,
'Category' => \App\GraphQL\Types\CategoryType::class
],
// some code
];
اختبار واجهة GraphQL عملياً
توفر لنا الحزمة واجهة تطوير جاهزة لاختبار الاستعلامات والعمليات. تأكد أولاً من أن حاويات Docker تعمل، ثم افتح الرابط:
http://localhost/graphiql

بعد ذلك يمكنك تجربة السيناريوهات التالية:
جلب مهمة واحدة

جلب قائمة مهام

إضافة مهمة جديدة

تحديث مهمة موجودة

حذف مهمة من قاعدة البيانات

لماذا يُعد GraphQL خياراً جيداً مع Laravel؟
عند استخدام GraphQL مع Laravel، تحصل على مرونة عالية في تحديد البيانات التي يحتاجها العميل بدقة، بدلاً من الاعتماد على نقاط نهاية متعددة كما هو الحال غالباً في REST. هذا يقلل من نقل البيانات غير الضرورية، ويمنح الواجهة الأمامية تحكماً أفضل في شكل الاستجابة.
كما أن دمج Eloquent مع GraphQL يجعل بناء العلاقات بين النماذج أكثر سلاسة، خاصة في المشاريع التي تحتوي على بنية بيانات مترابطة.
الخلاصة التقنية
بناء واجهة GraphQL API في Laravel ليس معقداً كما يبدو، خصوصاً عند استخدام حزمة rebing/graphql-laravel. الفكرة الأساسية تعتمد على ثلاثة مكونات واضحة: Types لتعريف شكل البيانات، وQueries لجلبها، وMutations لتعديلها. من الناحية التقنية، هذا الأسلوب مناسب جداً للمشاريع التي تحتاج إلى مرونة عالية في الاستهلاك من تطبيقات الويب أو الجوال، مع قابلية توسع ممتازة وتنظيم نظيف للكود.