استخدام مكتبة Fractal في إطاري العمل Laravel و Lumen لإنشاء استجابات للواجهات البرمجية


محمد طاهر الموسوي

اكتشفت مؤخرًا المتعة الكبيرة في استخدام مكتبة [Fractal المقدّمة من PHP League] لإنشاء استجابات للواجهات البرمجية التي تعطي مخرجات من نوع JSON وذلك عند العمل على نماذج Eloquent في إطاري العمل Laravel و Lumen للحصول على المخرجات التي أرغب بها. هذه الحزمة مشابهة إلى حدٍّ كبير لمكتبة ActiveModel Serializer في إطار العمل Rails، إذ إنّها تتيح لك التحكّم الدقيق بالمخرجات التي ترغب في وصولها إلى المستخدم، إضافة إلى أنّها تفصل الشيفرة المسؤولة عن إنشاء كائن JSON عن نموذج Eloquent وهذا أمر في غاية الأهمية.

والآن بعد أن أتيح لي تطوير عدد من الواجهات البرمجية لفترة زمنية لا بأس بها، خطر لي أن أتحدّث عن الطرق التي اتبعتها في ربط Fractal مع تطبيقات Laravel و Lumen (تختلف طريقة الربط في إطار Lumen لأنّه لا يدعم ماكروات Response).

المُسَلسِلات المخصّصة Costum Serializers

بادئ ذي بدء، تدعم Fractal عددًا من المُسَلسِلات Serializers لتكوين بنية خاصة بالاستجابة، وتستخدم بصورة افتراضية المسَلسِل الذي يدعى بـ DataArraySerializer والذي يضيف مفتاح جذر root key يحمل الاسم data إلى الاستجابة، لذا إن كنت لا تمانع من استخدام هذا المُسَلسِل فيمكنك تجاوز هذه الخطوة. أما لو كنت ترغب في استخدام مُسَلسِل آخر فعليك تسجيل الصنف Manager مع حاوية IoC في مزوّد الخدمة AppServiceProvider أو في مزوّد خدمة آخر إن كنت ترى أنّه يناسبك أكثر، ولكن عليك الانتباه إلى أنّ دعم JSON-API غير مكتمل حتى الآن (المقالة الأصلية نشرت سنة 2015. (المترجم)).

    public function register()
{
    $this->app->bind('League\Fractal\Manager', function($app) {
        $manager = new \League\Fractal\Manager;

        // Use the serializer of your choice.
        $manager->setSerializer(new \App\Http\Serializers\RootSerializer);

        return $manager;
    });
}

إنشاء المحوّلات Transformers

بعد ذلك سنحتاج إلى إنشاء بعض المحوّلات - محوّل لكل نوع من أنواع نماذج Eloquent التي ترغب في تخريجها بواسطة الواجهة البرمجية - وفي مثالنا هذا سأستخدم مدوّنة حيث يضمّ النموذج AppUser عددًا من نماذج AppPost وسنعرض المستخدم مع المقالات الخاصّة به. أنشأت مجلّدً للتحويلات داخل مجلّد Http: app/Http/Transoformers، والآن يمكنني إنشاء محوّلين، الأول لنموذج User والثاني لنموذج Post.

محوّل المستخدمين User سيكون سهلًا، إذ أنّه سيُرجع الحقول المطلوبة من قبل واجهتي البرمجية. في هذا المثال، يمتلك النموذج User خاصّية is_admin وهي عبارة عن قيمة منطقية boolean. ويمكنني التعبير عن ذلك في المحوّل بواسطة العبارة (bool) $user->is_admin ولكن اعتبارًا من الإصدار 5.0 وما بعده من Laravel أصبح بالإمكان استخدام خاصّية $casts في النموذج وسيكون Eloquent قادرًا بعدها على معالجة عملية الوصف بالنيابة عنك. يستحسن معالجة مثل هذه الأمور ضمن النموذج لأنّها ستكون متاحة في التطبيق برمّته.

<?php namespace App\Http\Transformers;

use App\User;
use League\Fractal\TransformerAbstract;

class UserTransformer extends TransformerAbstract
{
    /**
     * Turn this item object into a generic array.
     *
     * @param  \App\User  $user
     * @return array
     */
    public function transform(User $user)
    {
        return [
            'id'         => $user->id,
            'first_name' => $user->first_name,
            'last_name'  => $user->last_name,
            'email'      => $user->email,
            'is_admin'   => $user->is_admin,
            'created_at' => $post->created_at->toDateTimeString(),
            'updated_at' => $post->updated_at->toDateTimeString()
        ];
    }
}

بما أنّ خصائص التاريخ في نماذج Eloquent هي نسخ من مكتبة Carbon يمكن إذًا استخدام الدوال المساعدة التي تقدّمها هذه المكتبة لإرجاع التاريخ بالصيغة التي نرغب بها. وفي مثالنا سنستخدم الدالة toDateTimeString() وسيكون التاريخ بالصيغة التالية: Y-m-d H:i:s (السنة - الشهر - اليوم الساعة:الدقائق:الثواني).

والآن سنحتاج إلى محوّل لأجل نموذج Post، وسيكون هذا المحوّل مختلفًا عن السابق لأنّنا نرغب في تضمين النموذج User المرتبط بنموذج Post مع هذه الاستجابة.

<?php

namespace App\Http\Transforers;

use App\Post;
use League\Fractal\TransformerAbstract;

class PostTransformer extends TransformerAbstract
{
    /**
     * List of resources possible to include
     *
     * @var array
     */
    protected $availableIncludes = ['user'];

    /**
     * List of resources to automatically include.
     *
     * @var array
     */
    protected $defaultIncludes = ['user'];

    /**
     * Turn this item object into a generic array.
     *
     * @param  \App\Post  $post
     * @return array
     */
    public function transform(Post $post)
    {
        return [
            'id'           => $post->id,
            'title'        => $post->title,
            'content'      => $post->content,
            'created_at'   => $post->created_at->toDateTimeString(),
            'updated_at'   => $post->updated_at->toDateTimeString(),
            'published_at' => $post->published_at->toDateTimeString()
        ];
    }

    /**
     * Include user.
     *
     * @param  \App\Post  $post
     * @return League\Fractal\ItemResource
     */
    public function includeLevels(Post $post)
    {
        return $this->item($post->user, new UserTransformer);
    }
}

لاحظ أنّه بالإمكان إرجاع $this->collection داخل المحوّل إن كنت ترغب في ربط مجموعة بدل من عنصر واحد.

إنشاء الاستجابات في Laravel

عادة ما أنشئ مزوّد خدمة Fractal FractalServiceProvider والذي يضمّ التابع register() الذي قمت بوصفه سابقًا، ثم أسجّل بعض الماكروات في التابع boot() كما تلاحظ هنا. سيؤدي ذلك إلى إضافة تابعين إضافيين إلى معمل (factory) Response في Laravel وسيسهّل إرجاع الاستجابات على هيئة عناصر أو مجموعات.

لاحظ أنّي حدّدت نوع (type hint) المحوّل ليكون نسخة من الصنف TransformerAbstract وذلك لأنّي أنشئ دائمًا المحوّلات من الاستجابات بدلًا من صيغة الدالة المغلقة closure لأنّي أرى بأنّ هذه الطريقة ستحافظ على ترتيب الشيفرات التي أكتبها. لكن إن كنت تفضل استخدام الدوال المغلقة بدلًا من المحوّلات فيمكنك بكل ساطة أن لا تحدد نوع المحوّل ضمن المعاملات.

public function boot()
{
    $fractal = $this->app->make('League\Fractal\Manager');

    response()->macro('item', function ($item, \League\Fractal\TransformerAbstract $transformer, $status = 200, array $headers = []) use ($fractal) {
        $resource = new \League\Fractal\Resource\Item($item, $transformer);

        return response()->json(
            $fractal->createData($resource)->toArray(),
            $status,
            $headers
        );
    });

    response()->macro('collection', function ($collection, \League\Fractal\TransformerAbstract $transformer, $status = 200, array $headers = []) use ($fractal) {
        $resource = new \League\Fractal\Resource\Collection($collection, $transformer);

        return response()->json(
            $fractal->createData($resource)->toArray(),
            $status,
            $headers
        );
    });
}

والآن أصبح من السهل إنشاء استجابة واحدة (لمستخدم واحد مثلًا) ومجموعة من الاستجابات (لجميع مقالات المدوّنة مثلًا).

/**
 * GET /users/1
 *
 * @param  int  $userId
 * @return \Illuminate\Http\Response
 */
public function showUser($userId)
{
    $user = \App\User::findOrFail($userId);

    return response()->item($user, new \App\Http\Transformers\UserTransformer);
}

/**
 * GET /posts
 *
 * @return \Illuminate\Http\Response
 */
public function showPosts()
{
    $posts = \App\Post::with('user')->get();

    return response()->collection($posts, new \App\Http\Transformers\PostTransformer);
}

إن كنت ترغب في استخدام حالة استجابة HTTP أخرى مثل 201 (تم الإنشاء) أو 204 (لا يوجد محتوى) يمكنك وبكل سهولة تمرير رمز الحالة كمعامل ثالث إلى الماكرو.

إنشاء الاستجابات في Lumen

يتطلّب إنشاء الاستجابات في Lumen اتباع أسلوب مختلف قليلًا؛ ذلك لأنّ Lumen لا يدعم الماكروات في معمل Response؛ لذا سأضيف هذه التوابع إلى المتحكّم الرئيسي في الواجهة البرمجية.

/**
 * Create the response for an item.
 *
 * @param  mixed                                $item
 * @param  \League\Fractal\TransformerAbstract  $transformer
 * @param  int                                  $status
 * @param  array                                $headers
 * @return Response
 */
protected function buildItemResponse($item, \League\Fractal\TransformerAbstract $transformer, $status = 200, array $headers = [])
{
    $resource = new \League\Fractal\Resource\Item($item, $transformer);

    return $this->buildResourceResponse($resource, $status, $headers);
}

/**
 * Create the response for a collection.
 *
 * @param  mixed                                $collection
 * @param  \League\Fractal\TransformerAbstract  $transformer
 * @param  int                                  $status
 * @param  array                                $headers
 * @return Response
 */
protected function buildCollectionResponse($collection, \League\Fractal\TransformerAbstract $transformer, $status = 200, array $headers = [])
{
    $resource = new \League\Fractal\Resource\Collection($collection, $transformer);

    return $this->buildResourceResponse($resource, $status, $headers);
}

/**
 * Create the response for a resource.
 *
 * @param  \League\Fractal\Resource\ResourceAbstract  $resource
 * @param  int                                        $status
 * @param  array                                      $headers
 * @return Response
 */
protected function buildResourceResponse(\League\Fractal\Resource\ResourceAbstract $resource, $status = 200, array $headers = [])
{
    $fractal = app('League\Fractal\Manager');

    return response()->json(
        $fractal->createData($resource)->toArray(),
        $status,
        $headers
    );
}

يمكنك الآن استدعاء أي تابع من هذه التوابع في أي متحكّم موروث من هذا المتحكّم لإنشاء نفس الاستجابات التي أنشأناها مع Laravel.

return $this->buildItemResponse($user, new \App\Http\Transformers\UserTransformer);

return $this->buildCollectionResponse($posts, new \App\Http\Transformers\PostTransformer);

ترجمة - وبتصرّف - للمقال Using Fractal with Laravel and Lumen لصاحبه Dwight Conrad Watson.





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن