天天看點

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

首先composer安裝擴充包:folklore/graphql(github-most stars),然後最好是不管laravel哪個版本先去config\app.php添加個provider,即添加這樣一句:

Folklore\GraphQL\ServiceProvider::class,
           

然後再去publish配置

php artisan vendor:publish --provider="Folklore\GraphQL\ServiceProvider"
           

因為理論上laravel5.5以上版本會自動搞定,但是我之前沒添加provider就publish不完全。

Publish後就能在config\下看到多了一個graphql.php檔案,這個配置檔案不像其他的一次性就能修改好,而是根據項目需要所産生的Query和Type都需要在裡面“備案”一下,是以需要經常修改,下面會講到。

接下來開始寫接口,首先要寫Type設定以某種格式(int,string等内置或根據需要自設)傳回某個model的哪些字段,個人了解這裡的Type類似于DingoAPI的Transformer的作用,具體自行了解。

以活動和門票舉例,活動與門票為一對多關系,在Activity和Ticket的model裡當然要有相應的關聯方法,而且方法命名要合乎邏輯才行,比如這裡:

Activity.php:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

Ticket.php:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

下面直接上ActivityType和TicketType的代碼,直接在代碼裡注釋了

ActivityType.php

<?php
/**
 * Created by Lilei.
 * Date: 2018/11/14
 * Time: 13:40
 * Introduction:
 */

namespace App\GraphQL\Type;

use App\Models\Activity;
use Folklore\GraphQL\Support\Facades\GraphQL;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;

class ActivityType extends GraphQLType
{
    protected $attributes = [
        'name' => 'activity',  // 該處設定要準确有意義,配置檔案中備案時要用到
        'description' => '活動',
        'model' => Activity::class
    ];

    /**
     * 定義傳回的字段接口,即可以通過你寫的接口擷取到該model哪些字段
     * @return array
     */
    public function fields()
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int()),  // nonNull是不能為空的意思,即該字段值為空時會查詢失敗
                'description' => '活動id'
            ],
            'title' => [
                'type' => Type::string(),
                'description' => '活動主題'
            ],
            'description' => [
                'type' => Type::string(),
                'description' => '活動簡介'
            ],
            'address' => [
                'type' => Type::string(),
                'description' => '活動舉辦位址'
            ],
            'holding_on' => [
                'type' => Type::string(),
                'description' => '活動舉辦時間'
            ],
            'started_at' => [
                'type' => Type::string(),
                'description' => '活動報名開始時間'
            ],
            'ended_at' => [
                'type' => Type::string(),
                'description' => '活動報名結束時間'
            ],
            'created_at' => [
                'type' => Type::string(),
                'description' => '建立時間'
            ],
            'updated_at' => [
                'type' => Type::string(),
                'description' => '更新時間'
            ],
            'tickets' => [
                'type' => Type::listOf(GraphQL::type('ticket')),  // 此處即是一對多的關聯設定,listOf方法傳回關聯的所有資料
                'description' => '活動的門票類型'
            ]
        ];
    }

    // 因為laravel自動儲存的建立、更新、軟删除時間字段為Carbon對象,而上面隻能設定

//傳回字元串,是以要在下面對不符合指定類型的字段
    // 手動做一下處理,方法的命名嚴格按照下面格式來

    /**
     * @description 單獨處理created_at字段為string格式
     *
     * @author Lilei
     */
    protected function resolveCreatedAtField($root, $args)
    {
        return $root->created_at . '';
    }

    /**
     * @description 單獨處理updated_at字段為string格式
     *
     * @author Lilei
     */
    protected function resolveUpdatedAtField($root, $args)
    {
        return $root->updated_at . '';
    }
}

           

TicketType.php

<?php
/**
 * Created by Lilei.
 * Date: 2018/11/14
 * Time: 13:41
 * Introduction:
 */

namespace App\GraphQL\Type;

use App\Models\Ticket;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;

class TicketType extends GraphQLType
{
    protected $attributes = [
        'name' => 'Ticket',
        'description' => '',
        'model' => Ticket::class
    ];

    /**
     * 定義傳回的字段接口
     * @return array
     */
    public function fields()
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int()),
                'description' => '門票id'
            ],
            'name' => [
                'type' => Type::string(),
                'description' => '門票名字'
            ],
            'price' => [
                'type' => Type::string(),
                'description' => '門票價格'
            ],
            'total_num' => [
                'type' => Type::string(),
                'description' => '門票總數'
            ],
            'remain_num' => [
                'type' => Type::string(),
                'description' => '門票剩餘數量'
            ],
            'description' => [
                'type' => Type::string(),
                'description' => '門票說明'
            ],
            'activity_id' => [
                'type' => Type::string(),
                'description' => '所屬活動id'
            ],
            'started_at' => [
                'type' => Type::string(),
                'description' => '門票開售時間'
            ],
            'ended_at' => [
                'type' => Type::string(),
                'description' => '門票售賣截止時間'
            ],
            'created_at' => [
                'type' => Type::string(),
                'description' => '建立時間'
            ],
            'updated_at' => [
                'type' => Type::string(),
                'description' => '更新時間'
            ]
        ];
    }

    /**
     * @description 單獨處理created_at字段為string格式
     *
     * @author Lilei
     */
    protected function resolveCreatedAtField($root, $args)
    {
        return $root->created_at . '';
    }

    /**
     * @description 單獨處理updated_at字段為string格式
     *
     * @author Lilei
     */
    protected function resolveUpdatedAtField($root, $args)
    {
        return $root->updated_at . '';
    }
}

           

Type主要設定某model可以被擷取哪些字段,而Query相當于查詢的入口,可以設定要擷取哪個model的資料,而且可以自定義查詢限定條件,比如limit,id=1等等幾乎sql能用的查詢條件,下面還是直接上擷取活動表和關聯的門票表的所有資料的代碼:

ActivitiesQuery.php

<?php
/**
 * Created by Lilei.
 * Date: 2018/11/14
 * Time: 13:41
 * Introduction:
 */

namespace App\GraphQL\Query;

use App\Models\Activity;
use Folklore\GraphQL\Support\Facades\GraphQL;
use Folklore\GraphQL\Support\Query;
use GraphQL\Type\Definition\Type;

class ActivitiesQuery extends Query
{
    // 給該查詢做命名标記,擷取所有為複數,擷取某個的詳情為單數(不是強制)
    protected $attributes = [
        'name' => 'activities'
    ];

    // 設定擷取類型
    public function type()
    {
        return Type::listOf(GraphQL::type('activity'));  // 擷取多個活動集合
        // return GraphQL::type('activity');  // 擷取單個活動
    }

    // 定義可選篩選條件
    public function args()
    {
        return [
            'id'          => ['name' => 'id', 'type' => Type::int()],
            'title'       => ['name' => 'title', 'type' => Type::string()],
            'description' => ['name' => 'description', 'type' => Type::string()],
            'limit'       => ['name' => 'limit', 'type' => Type::int()],
        ];
    }

    // 處理篩選條件的相應傳回結果
    public function resolve($root, $args)
    {
        if (isset($args['limit'])) {
            return Activity::limit($args['limit'])->get();
        }

        if (isset($args['id'])) {
            // return Activity::where('id' , $args['id'])->first();
            return Activity::where('id' , $args['id'])->get();
        }

        if (isset($args['title'])) {
            // return Activity::where('title', $args['title'])->first();
            return Activity::where('title', $args['title'])->get();
        }

        if (isset($args['description'])) {
            // return Activity::where('description', $args['description'])->first();
            return Activity::where('description', $args['description'])->get();
        }

        return Activity::all();
    }
           

}

(要寫TicketQuery.php的話模仿上面即可)

上述代碼其實同時包含了擷取所有活動和擷取單個活動的寫法,當然要記得擷取單個活動時修改檔案名和查詢名為單數。

這裡遇到的坑:嘗試擷取單個活動詳情時死活擷取不到,查了一圈資料,轉回來發現當要查詢一個活動時,肯定要有限制條件的,一般就是id=?了,代碼中的Model::where()->get()的寫法是網上資料的大多數寫法,當然get()方法在laravel中也很常用,但是它傳回的是一個Eloquent對象的集合(盡管限制id時也要傳回集合),而擷取單個時,type()方法已經改為傳回一個,相應的它也隻需要一個Eloquent對象而不是集合,是以我就在坑裡爬不上來了,還好誤打誤撞發現了這裡,不然就真的出不去了,是以将get()方法改為first()或find(id)方法即可。

Type和Query寫完,不能急着運作,要記得給他們備案,直接上圖

config\graphql.php:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql
GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

多說一句,配置檔案第一項的prefix其實可以修改graphql查詢的入口,即路由,建議初學預設,就像直接通路localhost/graphql?query=query+FetchActivities{activities{id,title}}就可以看到查詢效果了,當然測試的話最好是借助工具,操作比較簡單,可以直接給laravel裝

"noh4ck/graphiql": "@dev"的composer包,也可以Google浏覽器安裝graphql插件,都是同一個工具,具體使用可自行google,上兩張圖:

擷取所有活動及關聯的門票:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

根據id擷取單個具體活動和關聯的所有門票:(注意左邊查詢格式的差別)

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

以上是對laravel使用graphql查詢資料的簡單使用探索,後面探索修改資料後會繼續補充。

———————————————分割線———————————————

下面就接着記錄laravel使用graphql建立、修改、删除資料的簡單使用探索。

前面查詢對應的接口叫Query,這裡建立和修改還有删除都涉及對資料庫的寫操作,是以對應的接口叫Mutation。

還是之前的活動和門票案例,不用多解釋,看代碼應該就能明白。

這裡先說個前提,使用Mutation去建立一條新紀錄時必須保證對應的model裡面聲明了$fillable屬性數組,否則建立時會報錯,更新和删除至少我測試時沒有強制這一點。

代碼格式和之前的Query差不多,就不多解釋了,主要是resolve不同。

建立門票App\GraphQL\Mutation\CreateTicketMutation.php

<?php

namespace App\GraphQL\Mutation;

use App\Models\Ticket;
use Folklore\GraphQL\Support\Facades\GraphQL;
use Folklore\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;

class CreateTicketMutation extends Mutation
{
    protected $attributes = [
        'name' => 'createTicket',
        'description' => 'The mutation to create a ticket'
    ];

    public function type()
    {
        return GraphQL::type('ticket');
    }

    public function args()
    {
        return [
            'name' => [
                'name' => 'name',
                'type' => Type::nonNull(Type::string()),
                'rules' => ['required']  // 參數校驗
            ],
            'price' => [
                'name' => 'price',
                'type' => Type::nonNull(Type::float()),
                'rules' => ['required']
            ],
            'total_num' => [
                'name' => 'total_num',
                'type' => Type::nonNull(Type::int()),
                'rules' => ['required']
            ],
            'remain_num' => [
                'name' => 'remain_num',
                'type' => Type::nonNull(Type::int()),
                'rules' => ['required']
            ],
            'description' => [
                'name' => 'description',
                'type' => Type::nonNull(Type::string()),
                'rules' => ['required']
            ],
            'activity_id' => [
                'name' => 'activity_id',
                'type' => Type::nonNull(Type::int()),
                'rules' => ['required']
            ],
            'started_at' => [
                'name' => 'started_at',
                'type' => Type::nonNull(Type::string()),
                'rules' => ['required']
            ],
            'ended_at' => [
                'name' => 'ended_at',
                'type' => Type::nonNull(Type::string()),
                'rules' => ['required']
            ],
        ];
    }

    public function resolve($root, $args, $context, ResolveInfo $info)
    {
        $ticket = Ticket::create($args);

// 下面是文檔中的寫法,不知道是不是多對多時得這樣寫,但現在多對一好像用不到,直接按上面簡寫了,最後傳回剛建立的這條記錄;
        // $ticket = new Ticket($args);
        // $activity = Activity::find($args['activity_id']);
        // if (!$activity) return null;
        // $activity->tickets()->save($ticket);
        return $ticket;
    }
}
           

更新活動App\GraphQL\Mutation\UpdateActivityMutation.php

<?php

namespace App\GraphQL\Mutation;

use App\Models\Activity;
use Folklore\GraphQL\Support\Facades\GraphQL;
use Folklore\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;

class UpdateActivityMutation extends Mutation
{
    protected $attributes = [
        'name' => 'updateActivity',
        'description' => 'update activity'
    ];

    public function type()
    {
        return GraphQL::type('activity');
    }

    public function args()
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::nonNull(Type::int()),
                'rules' => ['required']  // 參數校驗方法之二
            ],
            'title' => [
                'name' => 'title',
                'type' => Type::nonNull(Type::string()),
                'rules' => ['required']
            ]
        ];
    }

    // 參數校驗方法之一
    // public function rules()
    // {
    //     return [
    //         'id' => ['required'],
    //         'title' => ['required', 'email']
    //     ];
    // }

    public function resolve($root, $args, $context, ResolveInfo $info)
    {
        $activity = Activity::find($args['id']);
        if (!$activity) {
            return null;
        }
        $activity->title = $args['title'];
        $activity->save();
        
        // 這裡如果需要更新的字段較多時,也可以直接Activity::update($args);
        // 不過要事先把$args['id']從數組中删除,因為id一般是自增的
        // 我想這樣的話model應該就得強制聲明$fillable屬性數組了吧,有興趣可以測試一下

        return $activity;
    }
}
           

删除門票:App\GraphQL\Mutation\deleteTicketMutation

<?php

namespace App\GraphQL\Mutation;

use App\Models\Ticket;
use Folklore\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class DeleteTicketMutation extends Mutation
{
    protected $attributes = [
        'name' => 'deleteTicket',
        'description' => 'The mutation to delete a ticket'
    ];

    public function type()
    {
        return Type::string();  // 注意此處的不同,删除操作會傳回影響記錄的行數,是以不能再傳回一個Type
    }

    public function args()
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::nonNull(Type::int()),
                'rules' => ['required']
            ],
            'name' => [
                'name' => 'name',
                'type' => Type::string(),
            ],
            'price' => [
                'name' => 'price',
                'type' => Type::float(),
            ],
            'total_num' => [
                'name' => 'total_num',
                'type' => Type::int(),
            ],
            'remain_num' => [
                'name' => 'remain_num',
                'type' => Type::int(),
            ],
            'description' => [
                'name' => 'description',
                'type' => Type::string(),
            ],
            'started_at' => [
                'name' => 'started_at',
                'type' => Type::string(),
            ],
            'ended_at' => [
                'name' => 'ended_at',
                'type' => Type::string(),
            ],
        ];
    }

    public function resolve($root, $args, $context, ResolveInfo $info)
    {
        if (isset($args['id'])) {
            return Ticket::destroy($args['id']);
        } else {
            throw new BadRequestHttpException("删除失敗");
        }
    }
}
           

以上代碼中涉及了兩種參數驗證的方法,都差不多,完全看興趣選擇即可

寫完後當然不要忘記把Mutation去config\graphql.php備案一下:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

下面就可以在graphiQL測試一下了:

建立門票:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

更新活動:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

删除活動:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

然後查詢全部活動看一下效果(資料太多,截圖不全):

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

先到此為止,如果後續有更進階的使用,會繼續補充。