سأشرح لكم في هذا المقال كيفية إنشاء تطبيق CRUD لواجهة برمجة تطبيقات RESTful باستخدام Laravel 11، مع التركيز على أهم الخطوات التي يجب اتباعها لضمان الالتزام بأفضل الممارسات.​

للقيام بالمطلوب سأحتاج لاتباع عدة خطوات في مشروع لارافيل حيث سأنشئ نموذج CRUD كامل لإدارة المنتجات عبر الواجهة البرمجية كما سأستخدم Repository Pattern لتنظيم الكود وأضيف ميزة التحقق من البيانات وأوحد شكل استجابات الـ API وفق التالي:


  • الخطوة 1: إعداد مشروع Laravel
  • الخطوة 2: إعداد قاعدة البيانات MySQL
  • الخطوة 3: إنشاء نموذج لمنتج Product
  • الخطوة 4: إعداد التهجير Migration
  • الخطوة 5: إنشاء واجهة ProductRepositoryInterface
  • الخطوة 6: تنفيذ الواجهة في صنف ProductRepository
  • الخطوة 7: ربط الواجهة مع التنفيذ
  • الخطوة 8: التحقق من صحة الطلبات
  • الخطوة 9: إنشاء صنف موحّد للاستجابة

هيا بنا نتعرف على تفاصيل العمل على كل خطوة

 الخطوة1: إعداد لارافيل

سأنشئ في البداية مشروع لارافيل جديد وسأسميه rest-api-crud، وسأحمل الحزم من النسخ الجاهزة المضغوطة dist بدلاً من تحميلها من مصدرها كما يلي:

composer create-project --prefer-dist laravel/laravel rest-api-crud

بعد تنفيذ هذا الأمر، سيتم إنشاء مجلد باسم rest-api-crud وتثبيت أحدث إصدار من Laravel بداخله وتجهيز المشروع لبدء التطوير

 الخطوة2: إعداد قاعدة بيانات MySQL

بشكل افتراضي، يكون الاتصال بقاعدة البيانات في Laravel 11 مضبوطًا على sqlite، أي أن القيمة التالية موجودة في ملف البيئة .env:

DB_CONNECTION=sqlite

لكني ساستخدام قاعدة بيانات MySQL في مشروعي لذا يجب علي تغيير هذه القيمة إلى:

DB_CONNECTION=mysql


بعد تغيير نوع الاتصال إلى MySQL، يجب أيضًا التأكد من ضبط القيم التالية:


DB_HOST=127.0.0.1

DB_PORT=3306

DB_DATABASE=crud

DB_USERNAME=root

DB_PASSWORD=

 الخطوة3: إنشاء نمودج Product Model مع الهجرات

لإنشاء نموذج يُمثّل المنتج Product في لارافيل، نستخدم الأمر التالي:

php artisan make:model Product -a

سينشئ لارافيل بعد تنفيذ هذا الأمر مجموعة من الملفات داخل مجلدات المشروع، تشمل

  • النموذج app/Models/Product.php  
  • ملف الهجرة لإنشاء جدول المنتجات database/migrations/xxxx_xx_xx_create_products_table.php  
  • وحدة التحكم app/Http/Controllers/ProductController.php  
  • ملفات إضافية مثل Factory وResource إن كانت مطلوبة


الخطوة4: الهجرات Migrations

اذهب إلى الملف التالي داخل مشروعك

atabase/migrations/YYYY_MM_DD_HHMMSS_create_products_table.php


تمثل  الأرقام في اسم الملف (YYYY_MM_DD_HHMMSS) تاريخ ووقت إنشاء ملف التهجير.

بعد ذلك، أحدث الدالة up لتكون بالشكل التالي:

 public function up(): void

{

    Schema::create('products', function (Blueprint $table) {

        $table->id();                  // عمود "id" تلقائي، يُمثل المفتاح الأساسي Primary Key

        $table->string('name');        // عمود لتخزين اسم المنتج

        $table->string('details');     // عمود لتخزين تفاصيل المنتج

        $table->timestamps();          // عمودان: created_at و updated_at لتتبع وقت الإنشاء والتعديل

    });

}



الخطوة5: إنشاء واجهة Interface لمستودع المنتج Product Repository

ننشئ واجهة Interface لمستودع نموذج المنتج Product كي نفصل بين الواجهة والتطبيق فهذا يساعدنا على كتابة كود أنظف وأسهل في الصيانة مستقبلاً.


 php artisan make:interface /Interfaces/ProductRepositoryInterface


هذا الأمر غير رسمي في Laravel لأن Artisan لا يحتوي على أمر make:interface افتراضيًا، لذا قد تحتاج إلى إنشاء الواجهة يدويًا.

تكون طريقة الإنشاء اليدوي من داخل مجلد app/Interfaces لذا ننشئه إذا لم يكن موجودًا، أنشئ ملفًا جديدًا باسم:

ProductRepositoryInterface.php



<?php


namespace App\Interfaces;


interface ProductRepositoryInterface

{

    public function index();                         // جلب قائمة المنتجات

    public function getById($id);                    // جلب منتج حسب المعرّف

    public function store(array $data);              // إنشاء منتج جديد

    public function update(array $data, $id);        // تعديل منتج موجود

    public function delete($id);                     // حذف منتج

}


تتيح لنا الواجهة فصل منطق التطبيق (التحكم) عن منطق البيانات (التخزين والاسترجاع). كما أنها تجعل الكود مرنًا إذا أردت لاحقًا تغيير مصدر البيانات مثل التحوّل من MySQL إلى API خارجي ويسهّل عملية اختبار الوحدة unit testing.


الخطوة6: إنشاء صنف class لمستودع المنتج Product Repository

نقوم بإنشاء صنف يقوم بتنفيذ الواجهة ProductRepositoryInterface التي أنشأناها في الخطوة السابقة.

php artisan make:class /Repositories/ProductRepository

 

لا يحتوي لارافيل بشكل افتراضي على أمر make:class، لذا يجب إنشاء الملف يدويًا داخل مجلد app/Repositories.

أنشئ ملفًا جديدًا داخل مجلد app/Repositories باسم ProductRepository.php وضع فيه الكود التالي:

<?php


namespace App\Repository;


use App\Models\Product;

use App\Interfaces\ProductRepositoryInterface;


class ProductRepository implements ProductRepositoryInterface

{

    public function index(){

        return Product::all(); // استرجاع كل المنتجات

    }


    public function getById($id){

        return Product::findOrFail($id); // البحث عن منتج أو إظهار خطأ إذا لم يوجد

    }


    public function store(array $data){

        return Product::create($data); // إنشاء منتج جديد

    }


    public function update(array $data, $id){

        return Product::whereId($id)->update($data); // تحديث منتج موجود حسب المعرّف

    }


    public function delete($id){

        Product::destroy($id); // حذف منتج بناءً على المعرّف

    }

}



الخطوة7: ربط الواجهة Interface بالتنفيذ Implementation

نحتاج إلى إخبار لاارفيل بأنه في كل مرة يطلب فيها الصنف ProductRepositoryInterface، يجب أن يستخدم الصنف ProductRepository لتنفيذ المهام. نستخدم لهذا الغرض مزوّد خدمة Service Provider.

php artisan make:provider RepositoryServiceProvider


هذا الأمر ينشئ ملف مزوّد خدمة جديد داخل المجلد app/Providers/RepositoryServiceProvider.php

سنقوم داخل هذا الملف بتسجيل ربط الواجهة مع التنفيذ داخل دالة ()register كالتالي:


$this->app->bind(ProductRepositoryInterface::class, ProductRepository::class);


كلما طلب أحد الصنف ProductRepositoryInterface، سيتم حقنه باستخدام الصنف ProductRepository.

فيما يلي الكود الكامل داخل RepositoryServiceProvider.php:


<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

use App\Interfaces\ProductRepositoryInterface;

use App\Repository\ProductRepository;


class RepositoryServiceProvider extends ServiceProvider

{

    /**

     * تسجيل الخدمات داخل الحاوية.

     */

    public function register(): void

    {

        $this->app->bind(ProductRepositoryInterface::class, ProductRepository::class);

    }


    /**

     * تمهيد الخدمات.

     */

    public function boot(): void

    {

        // لا حاجة لتعديل شيء هنا الآن

    }

}


يجب تسجيل مزود الخدمة هذا داخل ملف config/app.php تحت قسم providers من خلال الأمر التالي:


App\Providers\RepositoryServiceProvider::class,


الخطوة8: التحقق من صحة الطلب Request Validation

في هذه الخطوة نقوم بإنشاء صنفين مخصصين للتحقق من صحة البيانات المدخلة عند إنشاء منتج جديد أو تحديث منتج موجود، وهما:

  • StoreProductRequest: عند إنشاء منتج جديد
  • UpdateProductRequest: عند تحديث منتج موجود

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


فيما يلي كود StoreProductRequest للتحقق عند إنشاء منتج جديد

<?php


namespace App\Http\Requests;


use Illuminate\Foundation\Http\FormRequest;

use Illuminate\Http\Exceptions\HttpResponseException;

use Illuminate\Contracts\Validation\Validator;


class StoreProductRequest extends FormRequest

{

    // تحديد إن كان المستخدم مخوّلًا بتنفيذ هذا الطلب

    public function authorize(): bool

    {

        return true; // السماح للجميع بتنفيذ الطلب

    }


    // قواعد التحقق من البيانات المدخلة

    public function rules(): array

    {

        return [

            'name' => 'required',   // الحقل "name" مطلوب

            'details' => 'required' // الحقل "details" مطلوب

        ];

    }


    // ماذا يحدث إذا فشل التحقق؟

    public function failedValidation(Validator $validator)

    {

        throw new HttpResponseException(response()->json([

            'success'   => false,

            'message'   => 'Validation errors',     // رسالة الخطأ

            'data'      => $validator->errors()     // تفاصيل الأخطاء

        ]));

    }

}


وفيما يلي كود UpdateProductRequest للتحقق عند تحديث منتج:

<?php


namespace App\Http\Requests;


use Illuminate\Foundation\Http\FormRequest;

use Illuminate\Http\Exceptions\HttpResponseException;

use Illuminate\Contracts\Validation\Validator;


class UpdateProductRequest extends FormRequest

{

    public function authorize(): bool

    {

        return true;

    }


    public function rules(): array

    {

        return [

            'name' => 'required',

            'details' => 'required'

        ];

    }


    public function failedValidation(Validator $validator)

    {

        throw new HttpResponseException(response()->json([

            'success'   => false,

            'message'   => 'Validation errors',

            'data'      => $validator->errors()

        ]));

    }

}




ملاحظة مهمة: يجب إنشاء هذين الصنفين باستخدام الأوامر التالية:

php artisan make:request StoreProductRequest

php artisan make:request UpdateProductRequest



فاستخدام هذه الأصناف داخل الـ Controller بدلاً من Request $request العادي يضمن أن البيانات ستتحقق تلقائيًا قبل تنفيذ أي عملية.


الخطوة9: إنشاء صنف الاستجابة العامة ApiResponseClass


في هذه الخطوة، ننشئ صنف Class مشتركة لعودة الاستجابة في تطبيق الـ API، وهي تُعتبر أفضل ممارسة لأنها تسهل إدارة استجابات الخادم في عدة أماكن من التطبيق باستخدام نفس الطريقة.  يفيدنا هذا الصنف في  إدارة الأخطاء بشكل موحد: مثلًا، في حالة حدوث خطأ في قاعدة البيانات، يمكن استرجاع البيانات بسهولة باستخدام هذا الصنف كما يساعد في إرسال استجابة موحدة حيث يمكنك إرسال الاستجابة للعميل بتنسيق موحد، مما يسهل التعامل مع الاستجابات في واجهة المستخدم.

فيما يلي كود هذا الصنف 

<?php


namespace App\Classes;


use Illuminate\Support\Facades\DB;

use Illuminate\Http\Exceptions\HttpResponseException;

use Illuminate\Support\Facades\Log;


class ApiResponseClass

{

    // لإلغاء التغييرات في حالة حدوث خطأ والرجوع إلى الحالة السابقة

    public static function rollback($e, $message = "Something went wrong! Process not completed")

    {

        DB::rollBack();  // التراجع عن العمليات في قاعدة البيانات

        self::throw($e, $message);  // رمي الاستثناء مع رسالة الخطأ

    }


    // لرمي استثناء مع رسالة معينة

    public static function throw($e, $message = "Something went wrong! Process not completed")

    {

        Log::info($e);  // تسجيل الخطأ في الـ log

        throw new HttpResponseException(response()->json(["message" => $message], 500));  // رمي الاستثناء مع رسالة خطأ

    }


    // لإرسال استجابة ناجحة مع البيانات

    public static function sendResponse($result, $message, $code = 200)

    {

        $response = [

            'success' => true,   // تعني أن العملية كانت ناجحة

            'data'    => $result // البيانات التي سيتم إرسالها

        ];


        // إضافة رسالة استجابة إذا كانت موجودة

        if (!empty($message)) {

            $response['message'] = $message;

        }


        // إرسال الاستجابة كـ JSON مع الكود المناسب

        return response()->json($response, $code);

    }

}



لأشرح لك أهم الدوال الواردة فيه:

  • الدالة ()rollback: في حال حدوث خطأ، يتراجع عن العمليات في قاعدة البيانات باستخدام ()DB::rollBack، ثم يرمي استثناء مع رسالة الخطأ.
  • الدالة ()throw: ترمي استثناء جديد مع رسالة مخصصة. يتم تسجيل الخطأ في الـ log باستخدام ()Log::info وتُرسل الاستجابة للعميل مع رمز حالة 500 (خطأ في الخادم).
  • دالة ()sendResponse:  تُستخدم لإرسال استجابة ناجحة مع البيانات. إذا تم تمرير رسالة إضافية، يتم إضافتها أيضًا. تُرسل الاستجابة بتنسيق JSON مع رمز الحالة 200 بشكل افتراضي، لكن يمكن تغيير الرمز إذا لزم الأمر.
  • يمكنك استخدام هذا الصنف في أي مكان في التطبيق حيث تحتاج إلى إرسال استجابة موحدة، مثلًا في الـ Controllers أو الـ Repositories:


 ApiResponseClass::sendResponse($result, 'Data fetched successfully');


كما يمكن استخدامها لإدارة الأخطاء كما في المثال التالي:

try {

    // عملية قد تحتوي على خطأ

} catch (\Exception $e) {

    ApiResponseClass::rollback($e);

}



الخطوة10: إنشاء مورد المنتج Product Resource

في هذه الخطوة، سوف تنشئ مورد Resource لتمثيل المنتج بطريقة أكثر تنظيماً عندما يتم تحويله إلى استجابة JSON. تساعد هذه الممارسة في إبقاء البيانات منظمة وتسهل التحكم في الشكل الذي يتم به إرسال البيانات للعملاء.

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

إليك كود هذا الصنف:


<?php


namespace App\Http\Resources;


use Illuminate\Http\Request;

use Illuminate\Http\Resources\Json\JsonResource;


class ProductResource extends JsonResource

{

    /**

     * تحويل المورد إلى مصفوفة.

     *

     * @return array<string, mixed>

     */

    public function toArray(Request $request): array

    {

        return [

            'id'      => $this->id,      // معرف المنتج

            'name'    => $this->name,    // اسم المنتج

            'details' => $this->details  // تفاصيل المنتج

        ];

    }

}

يرث الصنف ProductResource من JsonResource المدمج في Laravel، وهو صنف مُعتمدة لتحويل البيانات إلى JSON بطريقة منظمة. وتعمل الدالة ()toArray على تحويل البيانات المستلمة إلى مصفوفة لتكون جاهزة للإرسال كـ JSON. ويتم تحديد الحقول التي سيتم إرجاعها في الاستجابة، مثل id, name, و details.


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

يمكنك استخدام مورد المنتج عند إرسال بيانات المنتج كاستجابة JSON من الـ Controller. على سبيل المثال:


use App\Http\Resources\ProductResource;


public function show($id)

{

    $product = Product::findOrFail($id);

    return new ProductResource($product);

}


في هذا المثال، عند استرجاع المنتج باستخدام الـ findOrFail، يتم تمرير الكائن المنتج إلى ProductResource لتحويله إلى استجابة JSON منظمة.


الخطوة التالية هي إنشاء الـ Controller واستخدام هذا المورد فيه


الخطوة11: إنشاء صنف المتحكم ProductController


في هذه الخطوة، سنقوم بإضافة بعض الأكواد إلى التحكم في المنتج باستخدام المستودع (Repository) الذي قمنا بإنشائه سابقًا. سنقوم بإنشاء بعض الوظائف الخاصة بـ CRUD (الإنشاء، القراءة، التحديث، والحذف) باستخدام الـ Repository المُعرف مسبقًا.


 الصنف ProductController هو المسؤول عن التعامل مع العمليات الخاصة بالمنتجات عبر الـ API، مثل إضافة منتج جديد، تحديثه، حذفه، وعرضه. يتم تمرير البيانات إلى المستودع بدلاً من العمل مباشرة مع النموذج (Model)، مما يساعد في تنظيم الكود وتقليل التعقيد.

فيما يلي كود الصنف:


<?php


namespace App\Http\Controllers;


use App\Models\Product;

use App\Http\Requests\StoreProductRequest;

use App\Http\Requests\UpdateProductRequest;

use App\Interfaces\ProductRepositoryInterface;

use App\Classes\ResponseClass;

use App\Http\Resources\ProductResource;

use Illuminate\Support\Facades\DB;


class ProductController extends Controller

{

    private ProductRepositoryInterface $productRepositoryInterface;


    public function __construct(ProductRepositoryInterface $productRepositoryInterface)

    {

        $this->productRepositoryInterface = $productRepositoryInterface;

    }


    /**

     * عرض قائمة المورد.

     */

    public function index()

    {

        $data = $this->productRepositoryInterface->index();


        return ResponseClass::sendResponse(ProductResource::collection($data),'',200);

    }


    /**

     * عرض النموذج لإنشاء مورد جديد.

     */

    public function create()

    {

        //

    }


    /**

     * تخزين مورد تم إنشاؤه حديثًا في التخزين.

     */

    public function store(StoreProductRequest $request)

    {

        $details = [

            'name' => $request->name,

            'details' => $request->details

        ];

        

        DB::beginTransaction();

        try {

            $product = $this->productRepositoryInterface->store($details);


            DB::commit();

            return ResponseClass::sendResponse(new ProductResource($product), 'تم إنشاء المنتج بنجاح', 201);

        } catch (\Exception $ex) {

            return ResponseClass::rollback($ex);

        }

    }


    /**

     * عرض المورد المحدد.

     */

    public function show($id)

    {

        $product = $this->productRepositoryInterface->getById($id);


        return ResponseClass::sendResponse(new ProductResource($product), '', 200);

    }


    /**

     * عرض النموذج لتحرير المورد المحدد.

     */

    public function edit(Product $product)

    {

        //

    }


    /**

     * تحديث المورد المحدد في التخزين.

     */

    public function update(UpdateProductRequest $request, $id)

    {

        $updateDetails = [

            'name' => $request->name,

            'details' => $request->details

        ];


        DB::beginTransaction();

        try {

            $product = $this->productRepositoryInterface->update($updateDetails, $id);


            DB::commit();

            return ResponseClass::sendResponse('تم تحديث المنتج بنجاح', '', 201);

        } catch (\Exception $ex) {

            return ResponseClass::rollback($ex);

        }

    }


    /**

     * إزالة المورد المحدد من التخزين.

     */

    public function destroy($id)

    {

        $this->productRepositoryInterface->delete($id);


        return ResponseClass::sendResponse('تم حذف المنتج بنجاح', '', 204);

    }

}



عرفت في البداية المتغير productRepositoryInterface$ وقمت بتمرير الـ Repository الخاص بالمنتجات إلى الـ Controller باستخدام الحقن التلقائي (Dependency Injection) في الدالة construct.

وفي الدالة ()index استرجعت جميع المنتجات باستخدام الـ ProductRepositoryInterface وأرجعتها في استجابة JSON باستخدام ProductResource.

أما في الدالة ()store  فسأتعامل مع الطلب لإنشاء منتج جديد وأتحقق من البيانات المدخلة باستخدام StoreProductRequest ثم تُضاف البيانات إلى قاعدة البيانات باستخدام الـ Repository وإذا كانت العملية ناجحة، يتم إرسال استجابة تحتوي على البيانات الجديدة للمنتج.

في الدالة show($id) أسترجع منتجًا واحدًا باستخدام معرف المنتج id. ويتم إرسال الاستجابة مع البيانات المنظمة باستخدام ProductResource.

في الدالة ()update أحديث بيانات المنتج باستخدام UpdateProductRequest وإذا تم التحديث بنجاح، يتم إرسال استجابة تفيد بالنجاح.

في الدالة destroy($id) أحذف المنتج بناءً على معرفه (id) باستخدام الـ Repository وأستخدم الدوال DB::beginTransaction و DB::commit لضمان تنفيذ العمليات في معاملة واحدة، مما يعني أنه في حال حدوث خطأ، يمكن التراجع عن العملية بالكامل باستخدام DB::rollBack.

تكمن أهمية استخدام نموذج المستودع Repository Pattern في أن هذا الأسلوب يساعدني في الفصل بين منطق التطبيق (Business Logic) و منطق قاعدة البيانات، مما يجعل الكود أسهل في الصيانة والتوسيع.


الخطوة12: إنشاء الموجهات Routes

tي هذه الخطوة، سنقوم بإعداد مسارات التوجية للواجهة البرمجية API والخاصة بالمنتجات باستخدام ProductController. سننفذ أمرًا لنشر ملف المسارات، ثم نربط كل دالة في المتحكم Controller مع المسارات المحددة.


الهدف من هذه الخطوة هو نشر ملف Routes للواجهة البرمجية API وربط كل دالة في الـ Controller مع مسار محدد باستخدام Route::apiResource.


أولًا، سأنفذ الأمر التالي في سطر الأوامر:

php artisan install:api


 يتيح هذا الأمر نشر ملفات المسارات الخاصة بالواجهة البرمجية API في مشروع لارافيل. هذه الخطوة مهمة لكي تتمكن من إضافة المسارات الخاصة بالواجهة البرمجية في الملف المناسب.


بعد ذلك، قم بإضافة الكود التالي إلى routes/api.php:


<?php


use Illuminate\Http\Request;

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\ProductController;


// مسار يُرجع معلومات المستخدم بناءً على المصادقة باستخدام "Sanctum"

Route::get('/user', function (Request $request) {

    return $request->user();

})->middleware('auth:sanctum');


// ربط دوال الـ Controller الخاصة بالمنتج مع المسارات

Route::apiResource('/products', ProductController::class);



يستخدم المسار Route::get('/user', function (Request $request): المصادقة باستخدام الحزمة Sanctum، ويُرجع معلومات المستخدم عندما يقوم بإرسال طلب GET إلى المسار user/ وتستخدم البرمجية الوسيطة middleware للتحقق من المصادقة (auth:sanctum).


أما الدالة Route::apiResource('/products', ProductController::class): فتُستخدم لتحديد مجموعة من المسارات الخاصة بالـ CRUD (إنشاء، قراءة، تحديث، وحذف) للمنتجات.


عند استخدام ()Route::apiResource, فإن لارفيل يقوم تلقائيًا بربط كل دالة في المتحكم Controller مع مسار الـ API المناسب (مثل: GET /products, POST /products, PUT /products/{id}, DELETE /products/{id}).


تكمن أهمية استخدام Route::apiResource بأنها تمكنك من توفير جميع المسارات اللازمة للـ CRUD بشكل تلقائي، مما يوفر عليك الوقت والجهد في كتابة المسارات يدويًا.


تشغيل المشروع

انتهيت الآن من كل الخطوات الخاصة بإنشاء الوجهة البرمجية ويمكنك تشغيل المشروع واختبار مسارات التوجيه أو نقاط الوصول الخاصة بها عن طريق إرسال طلبات إلى المسارات التي أنشأناها للتعامل مع المنتج. 



Previous Post
No Comment
Add Comment
comment url