آموزش توسعهی GraphQL API با فریمورک Laravel
۱۱ خرداد ۱۴۰۰
اگر شما هم بهتازگی مسیر خود را بهعنوان یک توسعه دهندهی بکاند شروع کردهاید ممکن است کار با تکنولوژیهایی مانند GraphQL و Docker ترسناک بهنظر برسد. بههمین منظور میخواهیم در این مقاله به شما نحوهی ایجاد GraphQL API با فریمورک Laravel را آموزش دهیم تا متوجه شوید یادگیری این تکنولوژیها از آنچه که فکر میکردهاید بسیار سادهتر است.
البته باید اضافه کرد که در این مقاله به ریزجزئیات کار با فریمورک Laravel پرداخته نمیشود و شما میتوانید با مطالعهی مقالهی آموزش مقدماتی فریمورک Laravel به مباحث مقدماتی این فریمورک تسلط پیدا کنید.
پیش نیازها
پیشنیازهای ادامهی این مقاله به شرح زیر است و قبل از شروع کار باید از نصب بودن موارد زیر اطمینان حاصل کنید:
- +PHP 7
- Composer 2.0
- Docker
- Docker-Compose
همچنین فرض را بر این میگیریم که شما با مقدمات زبان PHP، فریمورک Laravel و تئوری GraphQL آشنا هستید.
توضیحاتی دربارهی برنامهی نهایی
این پروژه بسیار ساده فقط از دو Model با نامهای Quests و Categories تشکیل شده که Schema دیتابیس آن به شکل زیر است و شما میتوانید در انتهای این مقاله عملیات CRUD را با استفاده از GraphQL API بر روی این برنامهی ساده اجرا کنید.
پیادهسازی ساختار اولیهی پروژه
ما برای شروع به یک پروژهی Laravel نیاز داریم بنابراین با اجرای دستور زیر یک پروژهی Laravel با نام quest_journal
ایجاد میکنیم:
composer create-project laravel/laravel quest_journal
با اجرای موفقیتآمیز دستور فوق یک پوشهی جدید با نام quest_journal
در مسیر فعلی Terminal ایجاد خواهد شد و زمان آن فرا میرسد که برای پیکربندی Laravel Sail وارد مسیر پروژه شویم:
# Move into the project
cd quest_journal
# Install and configure laravel sail
php artisan sail:install
درهنگام نصب Laravel Sail از شما پرسیده میشود که میخواهید کدام Service را نصب کنید که با فشردن دکمهی Enter فقط MySQL نصب خواهد شد. پس از نصب Laravel Sail یک فایل جدید با نام docker-compose.yml
در مسیر پروژه ایجاد خواهد شد که به ما کمک میکند Containerهای مورد نیاز خود را با اجرای دستورهای زیر راهاندازی و اجرا کنیم:
# Run the containers
./vendor/bin/sail up -d
# Check if the containers are running
docker ps
اگر راهاندازی Containerها بدون خطا انجام شده باشد میتوانید آدرس localhost را در مرورگر خود باز کنید و صفحهی پیشفرض فریمورک Laravel برای شما نمایش داده خواهد شد.
اما همانطور که مشاهده میکنید برای استفاده از Laravel Sail باید مسیردهی فایل اجرایی را هر بار تکرار کنیم و برای مدیریت این موضوع از alias کمک خواهیم گرفت:
# in ~./zshrc or ~./bashrc
alias sail = 'bash vendor/bin/sail'
تا اینجای کار میتوان گفت که پیادهسازی ساختار اولیه پروژه انجام شده اما قبل از رفتن به مراحل بعدی باید چند پکیج را نصب کنیم:
# 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
درنهایت پس از نصب شدن rebing/graphql-laravel
میتوانیم با اجرای دستور زیر فایل پیکربندی GraphQL را در مسیر config/graphql.php
اضافه کنیم:
sail artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider"
ایجاد Modelها و Migrationها
بدون فوت وقت و توضیحات اضافی دربارهی نحوهی ایجاد یک Model در فریمورک Laravel بهسراغ اضافه کردن Category Model میرویم:
# Create model with migrations
sail artisan make:model -m Category
پس از اجرای دستور فوق، فایلهای مربوط به Model و Migration مورد نظر ما بهترتیب در مسیرهای App\Models
و database/migrations
ایجاد خواهد شد و ما در مرحلهی اول فیلدهای مورد نظر خود را به Migration ایجاد شده اضافه خواهیم کرد:
<?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');
}
}
پس از اضافه کردن فیلدها بهسراغ پیادهسازی Model ایجاد شده میرویم:
<?php
// App\Models\Category.php
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::class
روبرو خواهید شد اما جای نگرانی نیست. در قدم بعد با اجرای دستور زیر، Quest Model به پروژه اضافه خواهد شد:
sail artisan make:model -m Quest
اول بهسراغ فایل Migration میرویم و آن را بهشکل زیر تغییر میدهیم:
<?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');
}
}
و پس از تغییر فایل Migration مربوط به Quest بهسراغ فایل Model آن میرویم:
<?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
Seed کردن دیتابیس
بهعنوان برنامهنویسانی که در وارد کردن اطلاعات به دیتابیس بسیار تنبل هستند میخواهیم با اجرای دستورهای زیر از Factoryها برای وارد کردن اطلاعات به جدولهای Quest
و Category
کمک بگیریم:
# 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
در مسیر database/factories
میرویم که قرار است به ما در ایجاد Questهای fake کمک کند:
<?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
را پیادهسازی خواهیم کرد که بهطور کلی عملکرد آن سادهتر است و فقط باید titleهای مورد نیاز را برای ما ایجاد کند:
<?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 میتوانیم از classهای QuestFactory
و CategoryFactory
در فایل 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 API هستیم اما در ابتدا باید پوشهی GraphQL
را در پوشهی app
ایجاد کنیم. در مرحلهی بعد بایستی پوشههای Mutations
، Queries
و Types
را در پوشهی GraphQL
ایجاد کنیم تا ساختار نهایی پوشهها به شکل زیر باشد.
قبل از شروع کدنویسی بهتر است هدف هر پوشه را توضیح دهیم:
Mutations
: این پوشه شامل Classهایی میشود که عملیات insert، update و delete را مدیریت میکنند.Queries
: در این پوشه Classهایی قرار داده میشود که برخی دادههای مورد نیاز ما را از دیتابیس برنامه بازیابی میکنند.Types
: شما میتوانید این پوشه را جایی برای تعریف برخی Modelها درنظر بگیرید که نوع objectهابی که میتوانند از دیتابیس دریافت شوند را تعیین میکنند.
چگونگی تعریف Typeها
ما در پوشهی Types
دو Class با نامهای CategoryType
و QuestType
ایجاد خواهیم کرد و کاربرد پکیج rebing/graphql-laravel
اینجا مشخص خواهد شد:
<?php
// app\GraphQL\Types\CategoryType.php
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'
]
];
}
}
<?php
// app\GraphQL\Types\QuestType.php
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'
]
];
}
}
attributes
شامل اطلاعات اصلی درمورد Type و Model مورد استفادهی ما است و fields
تمام فیلدهایی که کاربر میتواند درخواست کند را return میکند.
چگونگی تعریف Queryها
پس از تعریف Typeها بهسراغ تعریف Queryها میرویم و برای هر Model، دو کوئری خواهیم داشت که مجموعا تعداد کوئریهای ما را به چهار عدد میرساند:
QuestQuery
QuestsQuery
CategoryQuery
CategoriesQuery
و ما این Queryها را بهشکل زیر در پوشهی Queries
قرار میدهیم.
پس از ایجاد همهی فایلها میتوانید کدهای مربوط به هر Query را در فایل مربوطه قرار دهید:
<?php
// app\GraphQL\Queries\Category\CategoriesQuery.php
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();
}
}
<?php
// app\GraphQL\Queries\Category\CategoryQuery.php
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\Quest\QuestQuery.php
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']);
}
}
<?php
// app\GraphQL\Queries\Quest\QuestsQuery.php
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();
}
}
برای درک بهتر کدهای فوق باید گفت که از فانکشن attributes
برای پیکربندی Queryها و از فانکشن type
برای تعیین نوع objectهایی که return میشوند استفاده میشود. در فانکشن args
نیز پارامترهای قابل قبول تعریف میشود که ما فقط به id
هر Quest نیاز داریم و درنهایت object مورد نظر ما در فانکشن resolve
با استفاده از eloquent
برگشت داده خواهد شد.
چگونگی تعریف Mutationها
ما برای هر Model به سه Class مختلف برای انجام عملیات Insert، Update و Delete نیاز خواهیم داشت که مجموع Classهای ما را به شش عدد میرساند:
CreateCategoryMutation
DeleteCategoryMutation
UpdateCategoryMutation
CreateQuestMutation
DeleteQuestMutation
UpdateQuestMutation
<?php
// app\GraphQl\Mutations\Category\CreateCategoryMutation.php
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.php
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.php
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.php
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.php
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.php
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;
}
}
فایل پیکربندی GraphQL
در نهایت پس از توسعهی Queryها، Mutationها و Typeها باید همهی آنها را در فایل config/graphql.php
که فایل پیکربندی GraphQL محسوب میشود، اضافه کرد:
<?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
];
تست برنامهی نهایی
اگر Docker container شما متوقف نشده باشد میتوانید آدرس http://localhost/graphiql را در مرورگر خود باز کرده و کوئریهای دلخواه خود را اجرا کنید.
منبع: https://www.freecodecamp.org/news/build-a-graphql-api-using-laravel/