基于 GraphQL 构建 Laravel API —— 基本使用篇


官方:https://www.howtographql.com/basics/0-introduction/

官方:https://graphql.org/learn/

参考:

Laravel API 系列教程(四):基于 GraphQL 构建 Laravel API —— 基本使用篇

Laravel API 系列教程(五):基于 GraphQL 构建 Laravel API —— 高级使用篇

https://auth0.com/blog/developing-and-securing-graphql-apis-with-laravel/


D:laragonwwwgraphql>composer require rebing/graphql-laravel
Using version ^5.1 for rebing/graphql-laravel
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing webonyx/graphql-php (v0.13.8): Downloading (100%)
  - Installing rebing/graphql-laravel (5.1.1): Downloading (100%)
webonyx/graphql-php suggests installing react/promise (To leverage async resolving on React PHP platform)
Writing lock file
Generating optimized autoload files
> IlluminateFoundationComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: facade/ignition
Discovered Package: fideloper/proxy
Discovered Package: fruitcake/laravel-cors
Discovered Package: laravel/tinker
Discovered Package: laravel/ui
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Discovered Package: rebing/graphql-laravel
Discovered Package: tymon/jwt-auth
Package manifest generated successfully.
3 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

D:laragonwwwgraphql>
D:laragonwwwgraphql>php artisan vendor:publish --provider="RebingGraphQLGraphQLServiceProvider"
Copied File [vendor
ebinggraphql-laravelconfigconfig.php] To [configgraphql.php]
Publishing complete.

D:laragonwwwgraphql>php artisan make:graphql:type UserType
Type created successfully.

D:laragonwwwgraphql>php artisan make:graphql:query UserQuery
Query created successfully.

D:laragonwwwgraphql>php artisan make:graphql:mutation UpdateUserPasswordMutation
Mutation created successfully.

D:laragonwwwgraphql>php artisan make:graphql:mutation UpdateUserEmailMutation
Mutation created successfully.

D:laragonwwwgraphql>php artisan make:graphql:type ArticleType
Type created successfully.

D:laragonwwwgraphql>php artisan make:graphql:enum ArticleStatusEnum
Enum created successfully.

D:laragonwwwgraphql>php artisan make:graphql:interface CharacterInterface
Interface created successfully.

D:laragonwwwgraphql>php artisan make:graphql:type HumanType
Type created successfully.

D:laragonwwwgraphql>php artisan make:graphql:field PictureField
Field created successfully.

graphql.php

<?php

declare(strict_types=1);

use exampleMutationExampleMutation;
use exampleQueryExampleQuery;
use exampleTypeExampleRelationType;
use exampleTypeExampleType;

return [

    // The prefix for routes
    'prefix' => 'graphql',

    // The routes to make GraphQL request. Either a string that will apply
    // to both query and mutation or an array containing the key 'query' and/or
    // 'mutation' with the according Route
    //
    // Example:
    //
    // Same route for both query and mutation
    //
    // 'routes' => 'path/to/query/{graphql_schema?}',
    //
    // or define each route
    //
    // 'routes' => [
    //     'query' => 'query/{graphql_schema?}',
    //     'mutation' => 'mutation/{graphql_schema?}',
    // ]
    //
    'routes' => '{graphql_schema?}',

    // The controller to use in GraphQL request. Either a string that will apply
    // to both query and mutation or an array containing the key 'query' and/or
    // 'mutation' with the according Controller and method
    //
    // Example:
    //
    // 'controllers' => [
    //     'query' => 'RebingGraphQLGraphQLController@query',
    //     'mutation' => 'RebingGraphQLGraphQLController@mutation'
    // ]
    //
    'controllers' => RebingGraphQLGraphQLController::class . '@query',

    // Any middleware for the graphql route group
    'middleware' => [],

    // Additional route group attributes
    //
    // Example:
    //
    // 'route_group_attributes' => ['guard' => 'api']
    //
    'route_group_attributes' => [],

    // The name of the default schema used when no argument is provided
    // to GraphQL::schema() or when the route is used without the graphql_schema
    // parameter.
    'default_schema' => 'default',

    // The schemas for query and/or mutation. It expects an array of schemas to provide
    // both the 'query' fields and the 'mutation' fields.
    //
    // You can also provide a middleware that will only apply to the given schema
    //
    // Example:
    //
    //  'schema' => 'default',
    //
    //  'schemas' => [
    //      'default' => [
    //          'query' => [
    //              'users' => 'AppGraphQLQueryUsersQuery'
    //          ],
    //          'mutation' => [
    //
    //          ]
    //      ],
    //      'user' => [
    //          'query' => [
    //              'profile' => 'AppGraphQLQueryProfileQuery'
    //          ],
    //          'mutation' => [
    //
    //          ],
    //          'middleware' => ['auth'],
    //      ],
    //      'user/me' => [
    //          'query' => [
    //              'profile' => 'AppGraphQLQueryMyProfileQuery'
    //          ],
    //          'mutation' => [
    //
    //          ],
    //          'middleware' => ['auth'],
    //      ],
    //  ]
    //
    'schemas' => [
        'default' => [
            'query' => [
                // 'example_query' => ExampleQuery::class,
                'users' => AppGraphQLQueriesUserQuery::class,
            ],
            'mutation' => [
                // 'example_mutation'  => ExampleMutation::class,
                'updateUserPassword' => AppGraphQLMutationsUpdateUserPasswordMutation::class,
                'updateUserEmail' => AppGraphQLMutationsUpdateUserEmailMutation::class,
            ],
            'middleware' => ['auth:api'],
            'method' => ['get', 'post'],
        ],
    ],

    // The types available in the application. You can then access it from the
    // facade like this: GraphQL::type('user')
    //
    // Example:
    //
    // 'types' => [
    //     'user' => 'AppGraphQLTypeUserType'
    // ]
    //
    'types' => [
        // 'example'           => ExampleType::class,
        // 'relation_example'  => ExampleRelationType::class,
        // RebingGraphQLSupportUploadType::class,
        'User' => AppGraphQLTypesUserType::class,
        'Article' => AppGraphQLTypesArticleType::class,
        'ArticleStatusEnum' => AppGraphQLEnumsArticleStatusEnum::class,
    ],

    // The types will be loaded on demand. Default is to load all types on each request
    // Can increase performance on schemes with many types
    // Presupposes the config type key to match the type class name property
    'lazyload_types' => false,

    // This callable will be passed the Error object for each errors GraphQL catch.
    // The method should return an array representing the error.
    // Typically:
    // [
    //     'message' => '',
    //     'locations' => []
    // ]
    'error_formatter' => ['RebingGraphQLGraphQL', 'formatError'],

    /*
     * Custom Error Handling
     *
     * Expected handler signature is: function (array $errors, callable $formatter): array
     *
     * The default handler will pass exceptions to laravel Error Handling mechanism
     */
    'errors_handler' => ['RebingGraphQLGraphQL', 'handleErrors'],

    // You can set the key, which will be used to retrieve the dynamic variables
    'params_key' => 'variables',

    /*
     * Options to limit the query complexity and depth. See the doc
     * @ https://webonyx.github.io/graphql-php/security
     * for details. Disabled by default.
     */
    'security' => [
        'query_max_complexity' => null,
        'query_max_depth' => null,
        'disable_introspection' => false,
    ],

    /*
     * You can define your own pagination type.
     * Reference RebingGraphQLSupportPaginationType::class
     */
    'pagination_type' => RebingGraphQLSupportPaginationType::class,

    /*
     * Config for GraphiQL (see (https://github.com/graphql/graphiql).
     */
    'graphiql' => [
        'prefix' => '/graphiql',
        'controller' => RebingGraphQLGraphQLController::class . '@graphiql',
        'middleware' => [],
        'view' => 'graphql::graphiql',
        'display' => env('ENABLE_GRAPHIQL', true),
    ],

    /*
     * Overrides the default field resolver
     * See http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver
     *
     * Example:
     *
     * ```php
     * 'defaultFieldResolver' => function ($root, $args, $context, $info) {
     * },
     * ```
     * or
     * ```php
     * 'defaultFieldResolver' => [SomeKlass::class, 'someMethod'],
     * ```
     */
    'defaultFieldResolver' => null,

    /*
     * Any headers that will be added to the response returned by the default controller
     */
    'headers' => [],

    /*
     * Any JSON encoding options when returning a response from the default controller
     * See http://php.net/manual/function.json-encode.php for the full list of options
     */
    'json_encoding_options' => 0,
];


User.php:

<?php

namespace App;

use IlluminateContractsAuthMustVerifyEmail;
use IlluminateDatabaseEloquentSoftDeletes;
use IlluminateFoundationAuthUser as Authenticatable;
use IlluminateNotificationsNotifiable;
use TymonJWTAuthContractsJWTSubject;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable, SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function articles()
    {
        return $this->hasMany(Article::class);
    }

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }
}


Article.php:

<?php

namespace App;

use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentSoftDeletes;

class Article extends Model
{
    use SoftDeletes;

    protected $fillable = ['user_id', 'title', 'body'];


    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

ArticleController.php:

<?php

namespace AppHttpControllers;

use AppArticle;
use IlluminateHttpRequest;
use IlluminateSupportFacadesValidator;

class ArticleController extends Controller
{
    public function __construct()
    {
//        $this->middleware('auth:api')->except(['show', 'index']);
        $this->middleware('auth:api')->except(['show']);
    }
    /**
     * Display a listing of the resource.
     *
     *
     *///@return IlluminateHttpResponse
    public function index(Request $request)
    {
        $user = $request->user();

        $articles = $user->articles()->get();
        return response()->json($articles);
    }


    /**
     * Store a newly created resource in storage.
     *
     * @param IlluminateHttpRequest $request
     *
     *///@return IlluminateHttpResponse
    public function store(Request $request)
    {

        $rules = [
            'title' => 'required|string|unique:articles|max:255',
            'body' => 'required|string|max:255',
        ];

        $message = [
            'title.required' => '必须输入title',
            'title.string' => 'title格式为字符串',
            'title.max' => 'title不要超过255',
            'title.unique' => 'title不可重复',
            'body.required' => '必须输入title',
            'body.string' => 'title格式为字符串',
            'body.max' => 'title不要超过255',
        ];

        $validator = Validator::make($request->all(), $rules, $message);

        if ($validator->fails()) {
            return response()->json($validator->errors()->getMessages(), 302);
        }

        $user = $request->user();

        if (!$user) {
            return response()->json([
                'error' => '请先登录',
            ], 401);
        }

        $data = $validator->validated();

        $data['user_id'] = $user->id;

        $article = Article::create($data);

        return response()->json($article, 201);

    }

    /**
     * Display the specified resource.
     *
     * @param AppArticle $article
     *
     */ //@return IlluminateHttpResponse
    public function show(Article $article)
    {
        return response()->json($article);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param IlluminateHttpRequest $request
     * @param AppArticle $article
     *
     */ // @return IlluminateHttpResponse
    public function update(Request $request, Article $article)
    {
        if ($article->user->id !== auth()->id()) {
            return response()->json([
                'error' => '没有权限操作不属于自己的文章',
            ], 402);
        }

        if ($request->get('title') === $article->title) {
            $rules = [
                'title' => 'required|string|max:255',
                'body' => 'required|string|max:255',
            ];
        } else {
            $rules = [
                'title' => 'required|string|unique:articles|max:255',
                'body' => 'required|string|max:255',
            ];
        }

        $message = [
            'title.required' => '必须输入title',
            'title.string' => 'title格式为字符串',
            'title.max' => 'title不要超过255',
            'title.unique' => 'title不可重复',
            'body.required' => '必须输入title',
            'body.string' => 'title格式为字符串',
            'body.max' => 'title不要超过255',
        ];

        $validator = Validator::make($request->all(), $rules, $message);

        if ($validator->fails()) {
            return response()->json($validator->errors()->getMessages(), 302);
        }

        $data = $validator->validated();
        if (!$article->update($data)) {
            return response()->json([
                'error' => 'Article update failed',
            ]);
        }

        return response()->json([
            'message' => 'Article updated',
        ], 200);

    }

    /**
     * Remove the specified resource from storage.
     *
     * @param AppArticle $article
     *
     */ //@return IlluminateHttpResponse
    public function destroy(Article $article)
    {
        $article->delete();
        return response()->json([
            'message' => 'Successfully Deleted',
        ], 200);
    }
}

ApiAuthAuthController.php

<?php

namespace AppHttpControllersApiAuth;

use AppHttpControllersController;
use AppUser;
use IlluminateHttpRequest;
use IlluminateSupportFacadesHash;
use IlluminateSupportFacadesValidator;

class AuthController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:api')->except(['login', 'register']);
    }

    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email' => 'required|string|email',
            'password' => 'required|string',
        ]);

        if (!$token = auth('api')->attempt($credentials)) {
            return response()->json([
                'error' => 'Wrong credentials',
            ], 401);
        }

        return $this->responseWithToken($token);
    }

    public function logout()
    {
        auth()->logout();
        return response()->json([
            'message' => 'Successfully Logged out',
        ], 200);
    }

    public function refresh()
    {
        return $this->responseWithToken(auth()->refresh());
    }

    public function me()
    {
        $user = auth('api')->user();

        return response()->json([
            'user' => $user,
            'token' => [
                'access_token' => auth('api')->login($user),
                'token_type' => 'bearer',
                'expires_in' => auth('api')->factory()->getTTL() * 60,
            ]
        ]);
    }

    public function register(Request $request)
    {
        $rules = [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|unique:users|max:255',
            'password' => 'required|string|confirmed|min:6|max:255',
        ];


        $validator = Validator::make($request->all(), $rules);

        if ($validator->fails()) {
            return response()->json($validator->errors()->getMessages(), 302);
        }

        $data = $validator->validated();

        $user = User::create(
            [
                'name' => $data['name'],
                'email' => $data['email'],
                'password' => Hash::make($data['password']),
            ]
        );

        $token = auth('api')->login($user);

        return $this->responseWithToken($token);
    }

    public function responseWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60,
        ]);
    }
}

GraphQLTypesUserType.php:

<?php

declare(strict_types=1);

namespace AppGraphQLTypes;

use AppGraphQLFieldsPictureField;
use GraphQLGraphQL;
use GraphQLTypeDefinitionType;
use RebingGraphQLSupportType as GraphQLType;

class UserType extends GraphQLType
{
    protected $attributes = [
        'name' => 'User',
        'description' => 'A type'
    ];

    public function fields(): array
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The id of the user',
            ],
            'email' => [
                'type' => Type::string(),
                'description' => 'The email of the user',
            ],
            'articles' => [
                'type' => Type::listOf(RebingGraphQLSupportFacadesGraphQL::type('Article')),
                'description' => 'The articles of the user',
            ],
            'picture' => PictureField::class
        ];
    }

    protected function resolveEmailField($root, $args)
    {
        return strtolower($root->email);
    }

    protected function resolveArticlesField($root, $args)
    {
        if (isset($args['id'])) {
            return $root->articles->where('id', $args['id']);
        }
        return $root->articles;
    }
}

GraphQLTypesArticleType.php:

<?php

declare(strict_types=1);

namespace AppGraphQLTypes;

use GraphQLTypeDefinitionType;
use RebingGraphQLSupportFacadesGraphQL;
use RebingGraphQLSupportType as GraphQLType;

class ArticleType extends GraphQLType
{
    protected $attributes = [
        'name' => 'Article',
        'description' => 'A type'
    ];

    public function fields(): array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::nonNull(Type::string()),
                'description' => 'The id of the article',
            ],
            'title' => [
                'name' => 'title',
                'type' => Type::nonNull(Type::string()),
                'description' => 'The title of the article',
            ],
            'body' => [
                'name' => 'body',
                'type' => Type::nonNull(Type::string()),
                'description' => 'The body of the article',
            ],
            'status' => [
                'name' => 'status',
                'type' => GraphQL::type('ArticleStatusEnum'),
                'description' => 'The status of the article',
            ]

        ];
    }
}

GraphQLTypesHumanType.php:

<?php

declare(strict_types=1);

namespace AppGraphQLTypes;

use GraphQLTypeDefinitionType;
use RebingGraphQLSupportFacadesGraphQL;
use RebingGraphQLSupportType as GraphQLType;

class HumanType extends GraphQLType
{
    protected $attributes = [
        'name' => 'Human',
        'description' => 'A Human'
    ];

    public function fields(): array
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int()),
                'description' => 'The id of the human.',
            ],
            'appearsIn' => [
                'type' => Type::nonNull(Type::listOf(GraphQL::type('Episode'))),
                'description' => 'A list of episodes in which the human has an appearance.'
            ],
            'totalCredits' => [
                'type' => Type::nonNull(Type::int()),
                'description' => 'The total amount of credits this human owns.'
            ]
        ];
    }

    public function interfaces(): array
    {
        return [
            GraphQL::type('Character')
        ];
    }
}

GraphQLQueriesUserQuery.php:

<?php

declare(strict_types=1);

namespace AppGraphQLQueries;

use AppUser;
use Closure;
use GraphQLTypeDefinitionResolveInfo;
use GraphQLTypeDefinitionType;
use IlluminateSupportFacadesAuth;
use RebingGraphQLSupportFacadesGraphQL;
use RebingGraphQLSupportQuery;
use RebingGraphQLSupportSelectFields;

class UserQuery extends Query
{
    protected $attributes = [
        'name' => 'user',
        'description' => 'A query'
    ];

    public function type(): Type
    {
        return Type::listOf(GraphQL::type('User'));
    }

    public function args(): array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::string(),
            ],
            'email' => [
                'name' => 'email',
                'type' => Type::string(),
            ],
        ];
    }

//    public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
//    {
////        /** @var SelectFields $fields */
////        $fields = $getSelectFields();
////        $select = $fields->getSelect();
////        $with = $fields->getRelations();
////
////        return [
////            'The user works',
////        ];
//        if (isset($args['id'])) {
//            return User::where('id', $args['id'])->get();
//        } elseif (isset($args['email'])) {
//            return User::where('email', $args['email'])->get();
//        } else {
//            return User::all();
//        }
//    }

    public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
        $fields = $resolveInfo->getFieldSelection($depth = 3);

        if (isset($args['id'])) {
            $user = User::where('id', $args['id']);
        } elseif (isset($args['email'])) {
            $user = User::where('email', $args['email']);
        } else {
            $user = User::query();
        }

        foreach ($fields as $field => $keys) {
            if ($field == 'articles') {
                $user->with('articles');
            }
        }

        return $user->get();
    }

}

GraphQLMutationsUpdateUserPasswordMutation.php:

<?php

declare(strict_types=1);

namespace AppGraphQLMutations;

use AppUser;
use Closure;
use GraphQLTypeDefinitionResolveInfo;
use GraphQLTypeDefinitionType;
use IlluminateSupportFacadesAuth;
use IlluminateSupportFacadesHash;
use RebingGraphQLSupportFacadesGraphQL;
use RebingGraphQLSupportMutation;
use RebingGraphQLSupportSelectFields;
use TymonJWTAuthJWTAuth;

class UpdateUserPasswordMutation extends Mutation
{
    protected $attributes = [
        'name' => 'updateUserPassword',
        'description' => 'A mutation'
    ];

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

    public function args(): array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::nonNull(Type::string()),
            ],
            'password' => [
                'name' => 'password',
                'type' => Type::nonNull(Type::string()),
            ],
        ];
    }

    public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
//        $fields = $getSelectFields();
//        $select = $fields->getSelect();
//        $with = $fields->getRelations();
//
//        return [];

        $user = User::find($args['id']);

        if (!$user) {
            return null;
        }
        $user->password = Hash::make($args['password']);

        $user->save();

        return $user;
    }

}

GraphQLMutationsUpdateUserEmailMutation.php:

<?php

declare(strict_types=1);

namespace AppGraphQLMutations;

use Closure;
use GraphQLTypeDefinitionResolveInfo;
use GraphQLTypeDefinitionType;
use AppUser;
use RebingGraphQLSupportFacadesGraphQL;
use RebingGraphQLSupportMutation;
use RebingGraphQLSupportSelectFields;

class UpdateUserEmailMutation extends Mutation
{
    protected $attributes = [
        'name' => 'updateUserEmail',
        'description' => 'A mutation'
    ];

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

    public function args(): array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::nonNull(Type::string()),
            ],
            'email' => [
                'name' => 'email',
                'type' => Type::nonNull(Type::string()),
            ],
        ];
    }

    public function rules(array $args = []): array
    {
        return [
            'id' => 'required',
            'email' => ['required', 'email'],
        ];
    }

    public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
//        $fields = $getSelectFields();
//        $select = $fields->getSelect();
//        $with = $fields->getRelations();
//
//        return [];

        $user = User::find($args['id']);

        if (!$user) {
            return null;
        }

        $user->email = $args['email'];

        $user->save();

        return $user;
    }
}

GraphQLInterfacesCharacterInterface.php:

<?php

declare(strict_types=1);

namespace AppGraphQLInterfaces;

use GraphQLTypeDefinitionStringType;
use GraphQLTypeDefinitionType;
use RebingGraphQLSupportFacadesGraphQL;
use RebingGraphQLSupportInterfaceType;

class CharacterInterface extends InterfaceType
{
    protected $attributes = [
        'name' => 'CharacterInterface',
        'description' => 'Character interface',
    ];

    public function resolveType($root): StringType
    {
        // Use the resolveType to resolve the Type which is implemented trough this interface
        $type = $root['type'];
        if ($type === 'human') {
            return GraphQL::type('Human');
        } else if ($type === 'droid') {
            return GraphQL::type('Droid');
        }
    }

    public function fields(): array
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int()),
                'description' => 'The id of the character.'
            ],
            'appearsIn' => [
                'type' => Type::nonNull(Type::listOf(GraphQL::type('Episode'))),
                'description' => 'A list of episodes in which the character has an appearance.'
            ],
        ];
    }
}

GraphQLFieldsPictureField.php:

<?php

declare(strict_types=1);

namespace AppGraphQLFields;

use GraphQLTypeDefinitionType;
use RebingGraphQLSupportField;

class PictureField extends Field
{
    protected $attributes = [
        'description' => 'A picture'
    ];

    public function type(): Type
    {
        return Type::string();
    }

    public function args(): array
    {
        return [
            'width' => [
                'type' => Type::int(),
                'description' => 'The width of the picture'
            ],
            'height' => [
                'type' => Type::int(),
                'description' => 'The height of the picture'
            ]
        ];
    }

    public function resolve($root, $args): string
    {
        $width = isset($args['width']) ? $args['width'] : 100;
        $height = isset($args['height']) ? $args['height'] : 100;
        return 'http://placehold.it/' . $width . 'x' . $height;
    }
}

GraphQLEnumsArticleStatusEnum.php:

<?php

declare(strict_types=1);

namespace AppGraphQLEnums;

use RebingGraphQLSupportEnumType;

class ArticleStatusEnum extends EnumType
{
    protected $enumObject = true;

    protected $attributes = [
        'name' => 'ArticleStatusEnum',
        'description' => 'Article Status Enum',
//        'values' => [
////            'TEST' => [
////                'value' => 1,
////                'description' => 'test',
////            ],
//            'APPROVED' => [
//                'value' => 1,
//                'description' => 'approved',
//            ],
//            'REJECT' => [
//                'value' => 0,
//                'description' => 'reject',
//            ]
//        ],
    ];

    public function values()
    {
        return [
            'APPROVED' => '1',
            'REJECT' => '0'
        ];
    }
}

appExceptionsHandler.php:

<?php

namespace AppExceptions;

use IlluminateAuthAuthenticationException;
use IlluminateFoundationExceptionsHandler as ExceptionHandler;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    /**
     * Report or log an exception.
     *
     * @param Throwable $exception
     * @return void
     *
     * @throws Exception
     */
    public function report(Throwable $exception)
    {
        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param IlluminateHttpRequest $request
     * @param Throwable $exception
     * @return SymfonyComponentHttpFoundationResponse
     *
     * @throws Throwable
     */
    public function render($request, Throwable $exception)
    {
        if ($exception instanceof AuthenticationException) {
            return response()->json(
                ['error' => '请登录'],
                401
            );
        }
        return parent::render($request, $exception);
    }
}

configauth.php:

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session", "token"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => AppUser::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | You may specify multiple password reset configurations if you have more
    | than one user table or model in the application and you want to have
    | separate password reset settings based on the specific user types.
    |
    | The expire time is the number of minutes that the reset token should be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Password Confirmation Timeout
    |--------------------------------------------------------------------------
    |
    | Here you may define the amount of seconds before a password confirmation
    | times out and the user is prompted to re-enter their password via the
    | confirmation screen. By default, the timeout lasts for three hours.
    |
    */

    'password_timeout' => 10800,

];

configjwt.php:

<?php

/*
 * This file is part of jwt-auth.
 *
 * (c) Sean Tymon <tymon148@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

return [

    /*
    |--------------------------------------------------------------------------
    | JWT Authentication Secret
    |--------------------------------------------------------------------------
    |
    | Don't forget to set this in your .env file, as it will be used to sign
    | your tokens. A helper command is provided for this:
    | `php artisan jwt:secret`
    |
    | Note: This will be used for Symmetric algorithms only (HMAC),
    | since RSA and ECDSA use a private/public key combo (See below).
    |
    */

    'secret' => env('JWT_SECRET'),

    /*
    |--------------------------------------------------------------------------
    | JWT Authentication Keys
    |--------------------------------------------------------------------------
    |
    | The algorithm you are using, will determine whether your tokens are
    | signed with a random string (defined in `JWT_SECRET`) or using the
    | following public & private keys.
    |
    | Symmetric Algorithms:
    | HS256, HS384 & HS512 will use `JWT_SECRET`.
    |
    | Asymmetric Algorithms:
    | RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below.
    |
    */

    'keys' => [

        /*
        |--------------------------------------------------------------------------
        | Public Key
        |--------------------------------------------------------------------------
        |
        | A path or resource to your public key.
        |
        | E.g. 'file://path/to/public/key'
        |
        */

        'public' => env('JWT_PUBLIC_KEY'),

        /*
        |--------------------------------------------------------------------------
        | Private Key
        |--------------------------------------------------------------------------
        |
        | A path or resource to your private key.
        |
        | E.g. 'file://path/to/private/key'
        |
        */

        'private' => env('JWT_PRIVATE_KEY'),

        /*
        |--------------------------------------------------------------------------
        | Passphrase
        |--------------------------------------------------------------------------
        |
        | The passphrase for your private key. Can be null if none set.
        |
        */

        'passphrase' => env('JWT_PASSPHRASE'),

    ],

    /*
    |--------------------------------------------------------------------------
    | JWT time to live
    |--------------------------------------------------------------------------
    |
    | Specify the length of time (in minutes) that the token will be valid for.
    | Defaults to 1 hour.
    |
    | You can also set this to null, to yield a never expiring token.
    | Some people may want this behaviour for e.g. a mobile app.
    | This is not particularly recommended, so make sure you have appropriate
    | systems in place to revoke the token if necessary.
    | Notice: If you set this to null you should remove 'exp' element from 'required_claims' list.
    |
    */

    'ttl' => env('JWT_TTL', 60),

    /*
    |--------------------------------------------------------------------------
    | Refresh time to live
    |--------------------------------------------------------------------------
    |
    | Specify the length of time (in minutes) that the token can be refreshed
    | within. I.E. The user can refresh their token within a 2 week window of
    | the original token being created until they must re-authenticate.
    | Defaults to 2 weeks.
    |
    | You can also set this to null, to yield an infinite refresh time.
    | Some may want this instead of never expiring tokens for e.g. a mobile app.
    | This is not particularly recommended, so make sure you have appropriate
    | systems in place to revoke the token if necessary.
    |
    */

    'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),

    /*
    |--------------------------------------------------------------------------
    | JWT hashing algorithm
    |--------------------------------------------------------------------------
    |
    | Specify the hashing algorithm that will be used to sign the token.
    |
    | See here: https://github.com/namshi/jose/tree/master/src/Namshi/JOSE/Signer/OpenSSL
    | for possible values.
    |
    */

    'algo' => env('JWT_ALGO', 'HS256'),

    /*
    |--------------------------------------------------------------------------
    | Required Claims
    |--------------------------------------------------------------------------
    |
    | Specify the required claims that must exist in any token.
    | A TokenInvalidException will be thrown if any of these claims are not
    | present in the payload.
    |
    */

    'required_claims' => [
        'iss',
        'iat',
        'exp',
        'nbf',
        'sub',
        'jti',
    ],

    /*
    |--------------------------------------------------------------------------
    | Persistent Claims
    |--------------------------------------------------------------------------
    |
    | Specify the claim keys to be persisted when refreshing a token.
    | `sub` and `iat` will automatically be persisted, in
    | addition to the these claims.
    |
    | Note: If a claim does not exist then it will be ignored.
    |
    */

    'persistent_claims' => [
        // 'foo',
        // 'bar',
    ],

    /*
    |--------------------------------------------------------------------------
    | Lock Subject
    |--------------------------------------------------------------------------
    |
    | This will determine whether a `prv` claim is automatically added to
    | the token. The purpose of this is to ensure that if you have multiple
    | authentication models e.g. `AppUser` & `AppOtherPerson`, then we
    | should prevent one authentication request from impersonating another,
    | if 2 tokens happen to have the same id across the 2 different models.
    |
    | Under specific circumstances, you may want to disable this behaviour
    | e.g. if you only have one authentication model, then you would save
    | a little on token size.
    |
    */

    'lock_subject' => true,

    /*
    |--------------------------------------------------------------------------
    | Leeway
    |--------------------------------------------------------------------------
    |
    | This property gives the jwt timestamp claims some "leeway".
    | Meaning that if you have any unavoidable slight clock skew on
    | any of your servers then this will afford you some level of cushioning.
    |
    | This applies to the claims `iat`, `nbf` and `exp`.
    |
    | Specify in seconds - only if you know you need it.
    |
    */

    'leeway' => env('JWT_LEEWAY', 0),

    /*
    |--------------------------------------------------------------------------
    | Blacklist Enabled
    |--------------------------------------------------------------------------
    |
    | In order to invalidate tokens, you must have the blacklist enabled.
    | If you do not want or need this functionality, then set this to false.
    |
    */

    'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),

    /*
    | -------------------------------------------------------------------------
    | Blacklist Grace Period
    | -------------------------------------------------------------------------
    |
    | When multiple concurrent requests are made with the same JWT,
    | it is possible that some of them fail, due to token regeneration
    | on every request.
    |
    | Set grace period in seconds to prevent parallel request failure.
    |
    */

    'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),

    /*
    |--------------------------------------------------------------------------
    | Cookies encryption
    |--------------------------------------------------------------------------
    |
    | By default Laravel encrypt cookies for security reason.
    | If you decide to not decrypt cookies, you will have to configure Laravel
    | to not encrypt your cookie token by adding its name into the $except
    | array available in the middleware "EncryptCookies" provided by Laravel.
    | see https://laravel.com/docs/master/responses#cookies-and-encryption
    | for details.
    |
    | Set it to true if you want to decrypt cookies.
    |
    */

    'decrypt_cookies' => false,

    /*
    |--------------------------------------------------------------------------
    | Providers
    |--------------------------------------------------------------------------
    |
    | Specify the various providers used throughout the package.
    |
    */

    'providers' => [

        /*
        |--------------------------------------------------------------------------
        | JWT Provider
        |--------------------------------------------------------------------------
        |
        | Specify the provider that is used to create and decode the tokens.
        |
        */

        'jwt' => TymonJWTAuthProvidersJWTLcobucci::class,

        /*
        |--------------------------------------------------------------------------
        | Authentication Provider
        |--------------------------------------------------------------------------
        |
        | Specify the provider that is used to authenticate users.
        |
        */

        'auth' => TymonJWTAuthProvidersAuthIlluminate::class,

        /*
        |--------------------------------------------------------------------------
        | Storage Provider
        |--------------------------------------------------------------------------
        |
        | Specify the provider that is used to store tokens in the blacklist.
        |
        */

        'storage' => TymonJWTAuthProvidersStorageIlluminate::class,

    ],

];

api.php:

<?php

use IlluminateHttpRequest;
use IlluminateSupportFacadesRoute;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|php
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

Route::apiResource('articles', 'ArticleController');

Route::post('login', 'ApiAuthAuthController@login')->middleware('guest')->name('login.api');
Route::post('logout', 'ApiAuthAuthController@logout')->middleware('auth:api')->name('logout.api');
Route::post('refresh', 'ApiAuthAuthController@refresh')->middleware('auth:api')->name('refresh.api');
Route::post('register', 'ApiAuthAuthController@register')->middleware('guest')->name('register.api');
Route::get('me', 'ApiAuthAuthController@me')->middleware('auth:api');


.env:

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:UkfFliQ/y1iTF4uW8QVQar7t855lRmD5Ap3i8rySShw=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=graphql
DB_USERNAME=root
DB_PASSWORD=

BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

JWT_SECRET=1Je1e5gh0ZxO5XJ7ScOJJZq6WAbX0Uez7yP5Vkvf1tyyUkioTziZ0aFpZ1tDx9KP

CreateUsersTable:

<?php

use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->softDeletes();
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

CreateArticlesTable

<?php

use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

class CreateArticlesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->unsignedBigInteger('user_id');
            $table->softDeletes();
            $table->timestamps();
        });
        Schema::table('articles', function (Blueprint $table) {
            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->cascadeOnDelete();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('articles');
    }
}

ArticleFactory.php:

<?php

/** @var IlluminateDatabaseEloquentFactory $factory */

use AppArticle;
use FakerGenerator as Faker;

$factory->define(Article::class, function (Faker $faker) {
    return [
        //
        'title' => $faker->sentence,
        'body' => $faker->paragraph,
    ];
});

ArticleTableSeeder.php:

<?php

use IlluminateDatabaseSeeder;

class ArticleTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        //
        factory(AppArticle::class, 50)->create(
            ['user_id' => 1]
        );
    }
}

DatabaseSeeder.php:

<?php

use IlluminateDatabaseSeeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(UserSeeder::class);
        $this->call(UserTableSeeder::class);
        $this->call(ArticleTableSeeder::class);
    }
}

UserTableSeeder.php:

<?php

use AppUser;
use IlluminateDatabaseSeeder;

class UserTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        //
        factory(User::class, 10)->create();
    }
}

Github:

https://github.com/dzkjz/graphql_learn1

https://github.com/dzkjz/graphql_learn1.git

原文地址:https://www.cnblogs.com/dzkjz/p/12796857.html