jarischaefer / hal-api
通过自动化常见任务来增强您的HATEOAS体验。
Requires
- php: >=7.1.0
Requires (Dev)
- fzaninotto/faker: ~1.4
- laravel/framework: 5.4.*
- mockery/mockery: dev-master
- phpunit/phpunit: ~5.7
This package is not auto-updated.
Last update: 2024-09-28 17:07:21 UTC
README
通过自动化常见任务来增强您的HATEOAS体验。
关于
此包基于Laravel 5。它旨在自动化RESTful API编程中的常见任务。这些文档可能不会与所有更改保持同步。
安装
需求
需要Laravel 5.4和PHP 7.1。
Composer
通过以下命令使用Composer安装包
composer require jarischaefer/hal-api:dev-master
或者在你的composer.json中包含以下内容。
"require": { "jarischaefer/hal-api": "dev-master" }
查看发行页面获取可用版本列表。
服务提供者
app.php
在config/app.php文件中注册服务提供者。
'providers' => [ Jarischaefer\HalApi\Providers\HalApiServiceProvider::class, ]
compile.php(可选步骤)
在config/compile.php文件中注册服务提供者。
'providers' => [ Jarischaefer\HalApi\Providers\HalApiServiceProvider::class, ]
运行 php artisan optimize --force
以编译优化的类加载器。
用法
简单控制器
此类控制器没有模型支持,不提供任何CRUD操作。典型用法是API的入口点。以下控制器应路由到API的根路径,并列出所有关系。
class HomeController extends HalApiController { public function index(HalApiRequestParameters $parameters) { return $this->responseFactory->json($this->createResponse($parameters)->build()); } }
资源控制器
资源控制器需要三个额外的组件
- 模型:资源的数据包含在模型中
- 仓库:仓库检索和存储模型
- 转换器:将模型转换为HAL表示
class UsersController extends HalApiResourceController { public static function getRelationName(): string { return 'users'; } public function __construct(HalApiControllerParameters $parameters, UserTransformer $transformer, UserRepository $repository) { parent::__construct($parameters, $transformer, $repository); } public function posts(HalApiRequestParameters $parameters, PostsController $postsController, User $user): Response { $posts = $user->posts()->paginate($parameters->getPerPage()); $response = $postsController->paginate($parameters, $posts)->build(); return $this->responseFactory->json($response); } } class PostsController extends HalApiResourceController { public static function getRelationName(): string { return 'posts'; } public function __construct(HalApiControllerParameters $parameters, PostTransformer $transformer, PostRepository $repository) { parent::__construct($parameters, $transformer, $repository); } }
模型
以下是一个简单的包含两个表的关系。用户与文章有一个一对多关系。
class User extends Model implements AuthenticatableContract, CanResetPasswordContract { use Authenticatable, CanResetPassword; /** * The attributes excluded from the model's JSON form. * * @var array */ protected $hidden = ['password', 'remember_token']; public function posts() { return $this->hasMany(Post::class); } } class Post extends Model { // ... public function user() { return $this->belongsTo(User::class); } }
仓库
你可以通过扩展HalApiEloquentRepository
并实现它的getModelClass()方法来创建一个兼容Eloquent的仓库。
class UserRepository extends HalApiEloquentRepository { public static function getModelClass(): string { return User::class; } } class PostRepository extends HalApiEloquentRepository { public static function getModelClass(): string { return Post::class; } }
可搜索的仓库
实现HalApiSearchRepository可以支持按字段搜索/过滤。有一个兼容Eloquent的仓库可用。不限制可搜索的字段可能会导致信息泄露。
class UserRepository extends HalApiEloquentSearchRepository { public static function getModelClass(): string { return User::class; } public static function searchableFields(): array { return [User::COLUMN_NAME]; } } class PostRepository extends HalApiEloquentSearchRepository { public static function getModelClass(): string { return Post::class; } public static function searchableFields(): array { return ['*']; } }
转换器
转换器在模型和控制器之间提供了一个额外的层。它们帮助您为单个项目或项目集合创建HAL响应。
class UserTransformer extends HalApiTransformer { public function transform(Model $model) { /** @var User $model */ return [ 'id' => (int)$model->id, 'username' => (string)$model->username, 'email' => (string)$model->email, 'firstname' => (string)$model->firstname, 'lastname' => (string)$model->lastname, 'disabled' => (bool)$model->disabled, ]; } } class PostTransformer extends HalApiTransformer { public function transform(Model $model) { /** @var Post $model */ return [ 'id' => (int)$model->id, 'title' => (string)$model->title, 'text' => (string)$model->text, 'user_id' => (int)$model->user_id, ]; } }
链接关系
重写转换器的getLinks方法允许您链接到相关资源。链接文章到其用户
class PostTransformer extends HalApiTransformer { private $userRoute; private $userRelation; public function __construct(LinkFactory $linkFactory, RepresentationFactory $representationFactory, RouteHelper $routeHelper, Route $self, Route $parent) { parent::__construct($linkFactory, $representationFactory, $routeHelper, $self, $parent); $this->userRoute = $routeHelper->byAction(UsersController::actionName(RouteHelper::SHOW)); $this->userRelation = UsersController::getRelation(RouteHelper::SHOW); } public function transform(Model $model) { /** @var Post $model */ return [ 'id' => (int)$model->id, 'title' => (string)$model->title, 'text' => (string)$model->text, 'user_id' => (int)$model->user_id, ]; } protected function getLinks(Model $model) { /** @var Post $model */ return [ $this->userRelation => $this->linkFactory->create($this->userRoute, $model->user_id), ]; } }
注意链接中的"users.show"关系。
{ "data": { "id": 123, "title": "Welcome!", "text": "Hello World", "user_id": 456 }, "_links": { "self": { "href": "http://hal-api.development/posts/123", "templated": true }, "parent": { "href": "http://hal-api.development/posts", "templated": false }, "users.show": { "href": "http://hal-api.development/users/456", "templated": true }, "posts.update": { "href": "http://hal-api.development/posts/123", "templated": true }, "posts.destroy": { "href": "http://hal-api.development/posts/123", "templated": true } }, "_embedded": { } }
嵌入关系
一旦两个不同的模型中的数据需要组合,链接方法就不太适用了。显示文章的作者(用户模型中的firstname和lastname)在超过十项(N+1 GET请求到所有"users.show"关系)的情况下变得不可行。嵌入相关数据基本上与预加载相同。
class PostTransformer extends HalApiTransformer { private $userTransformer; private $userRelation; public function __construct(LinkFactory $linkFactory, RepresentationFactory $representationFactory, RouteHelper $routeHelper, Route $self, Route $parent, UserTransformer $userTransformer) { parent::__construct($linkFactory, $representationFactory, $routeHelper, $self, $parent); $this->userTransformer = $userTransformer; $this->userRelation = UsersController::getRelation(RouteHelper::SHOW); } public function transform(Model $model) { /** @var Post $model */ return [ 'id' => (int)$model->id, 'title' => (string)$model->title, 'text' => (string)$model->text, 'user_id' => (int)$model->user_id, ]; } protected function getEmbedded(Model $model) { /** @var Post $model */ return [ $this->userRelation => $this->userTransformer->item($model->user), ]; } }
注意_embedded
字段中的"users.show"关系。
{ "data": { "id": 123, "title": "Welcome!", "text": "Hello World", "user_id": 456 }, "_links": { "self": { "href": "http://hal-api.development/posts/123", "templated": true }, "parent": { "href": "http://hal-api.development/posts", "templated": false }, "posts.update": { "href": "http://hal-api.development/posts/123", "templated": true }, "posts.destroy": { "href": "http://hal-api.development/posts/123", "templated": true } }, "_embedded": { "users.show": { "data": { "id": 456, "username": "foo-bar", "email": "foo.bar@example.com", "firstname": "foo", "lastname": "bar", "disabled": false }, "_links": { "self": { "href": "http://hal-api.development/users/456", "templated": true }, "parent": { "href": "http://hal-api.development/users", "templated": false }, "users.posts": { "href": "http://hal-api.development/users/456/posts", "templated": true }, "users.update": { "href": "http://hal-api.development/users/456", "templated": true }, "users.destroy": { "href": "http://hal-api.development/users/456", "templated": true } }, "_embedded": { } } } }
依赖关系配置
建议你在服务提供者中配置转换器的依赖关系
class MyServiceProvider extends ServiceProvider { public function boot(Router $router) { $this->app->singleton(UserTransformer::class, function (Illuminate\Contracts\Foundation\Application $application) { $linkFactory = $application->make(LinkFactory::class); $representationFactory = $application->make(RepresentationFactory::class); $routeHelper = $application->make(RouteHelper::class); $self = $routeHelper->byAction(UsersController::actionName(RouteHelper::SHOW)); $parent = $routeHelper->parent($self); return new UserTransformer($linkFactory, $representationFactory, $routeHelper, $self, $parent); }); $this->app->singleton(PostTransformer::class, function (Illuminate\Contracts\Foundation\Application $application) { $linkFactory = $application->make(LinkFactory::class); $representationFactory = $application->make(RepresentationFactory::class); $routeHelper = $application->make(RouteHelper::class); $self = $routeHelper->byAction(PostsController::actionName(RouteHelper::SHOW)); $parent = $routeHelper->parent($self); $userTransformer = $application->make(UserTransformer::class); return new PostTransformer($linkFactory, $representationFactory, $routeHelper, $self, $parent, $userTransformer); }); } }
routes.php
RouteHelper会自动为所有CRUD操作创建路由。
RouteHelper::make($router) ->get('/', HomeController::class, 'index') // Link GET / to the index method in HomeController ->resource('users', UsersController::class) // Start a new resource block ->get('posts', 'posts') // Link GET /users/{users}/posts to the posts method in UsersController ->done() // Close the resource block ->resource('posts', PostsController::class) ->done();
禁用CRUD操作和分页
RouteHelper::make($router) ->resource('users', UsersController::class, [RouteHelper::SHOW, RouteHelper::INDEX], false) ->done();
搜索/过滤
控制器仓库必须实现HalApiSearchRepository。
RouteHelper::make($router) ->resource('users', UsersController::class) ->searchable() ->done();
RouteServiceProvider
确保你在RouteServiceProvider中绑定所有路由参数。下面的回调根据请求方法处理丢失的参数。例如,对不存在的数据库记录的GET请求应返回404响应。对于PUT以外的所有其他HTTP方法也是如此。PUT简单地创建资源,如果它之前不存在。
public function boot(Router $router) { parent::boot($router); $callback = RouteHelper::getModelBindingCallback(); $router->model('users', User::class, $callback); $router->model('posts', Post::class, $callback); }
异常处理器
上面的回调如果找不到记录,会抛出NotFoundHttpException异常。为了创建适当的响应而不是错误页面,必须修改异常处理器。如下所示,根据捕获到的异常,会返回各种HTTP状态码,如404和422。
class Handler extends ExceptionHandler { public function report(Exception $e) { parent::report($e); } public function render($request, Exception $e) { switch (get_class($e)) { case ModelNotFoundException::class: return response('', Response::HTTP_NOT_FOUND); case NotFoundHttpException::class: return response('', Response::HTTP_NOT_FOUND); case BadPutRequestException::class: return response('', Response::HTTP_UNPROCESSABLE_ENTITY); case BadPostRequestException::class: return response('', Response::HTTP_UNPROCESSABLE_ENTITY); case TokenMismatchException::class: return response('', Response::HTTP_FORBIDDEN); case DatabaseConflictException::class: return response('', Response::HTTP_CONFLICT); case DatabaseSaveException::class: $this->report($e); return response('', Response::HTTP_UNPROCESSABLE_ENTITY); case FieldNotSearchableException::class: return response('', Response::HTTP_FORBIDDEN); default: $this->report($e); return Config::get('app.debug') ? parent::render($request, $e) : response('', Response::HTTP_INTERNAL_SERVER_ERROR); } } }
示例
特定模型的JSON(显示)
{ "data": { "id": 123, "username": "FB", "email": "foo.bar@example.com", "firstname": "foo", "lastname": "bar", "disabled": false }, "_links": { "self": { "href": "http://hal-api.development/users/123", "templated": true }, "parent": { "href": "http://hal-api.development/users", "templated": false }, "users.posts": { "href": "http://hal-api.development/users/123/posts", "templated": true }, "users.update": { "href": "http://hal-api.development/users/123", "templated": true }, "users.destroy": { "href": "http://hal-api.development/users/123", "templated": true } }, "_embedded": { } }
模型列表的JSON(索引)
{ "_links": { "self": { "href": "http://hal-api.development/users", "templated": false }, "parent": { "href": "http://hal-api.development", "templated": false }, "users.posts": { "href": "http://hal-api.development/users/{users}/posts", "templated": true }, "users.show": { "href": "http://hal-api.development/users/{users}", "templated": true }, "users.store": { "href": "http://hal-api.development/users", "templated": false }, "users.update": { "href": "http://hal-api.development/users/{users}", "templated": true }, "users.destroy": { "href": "http://hal-api.development/users/{users}", "templated": true }, "users.posts": { "href": "http://hal-api.development/users/{users}/posts", "templated": true }, "first": { "href": "http://hal-api.development/users?current_page=1", "templated": false }, "next": { "href": "http://hal-api.development/users?current_page=2", "templated": false }, "last": { "href": "http://hal-api.development/users?current_page=10", "templated": false } }, "_embedded": { "users.show": [ { "data": { "id": 123, "username": "FB", "email": "foo.bar@example.com", "firstname": "Foo", "lastname": "Bar", "disabled": false }, "_links": { "self": { "href": "http://hal-api.development/users/123", "templated": true }, "parent": { "href": "http://hal-api.development/users", "templated": false }, "users.posts": { "href": "http://hal-api.development/users/123/posts", "templated": true }, "users.update": { "href": "http://hal-api.development/users/123", "templated": true }, "users.destroy": { "href": "http://hal-api.development/users/123", "templated": true }, "users.posts": { "href": "http://hal-api.development/users/123/posts", "templated": true } }, "_embedded": { } }, { "data": { "id": 456, "username": "JD", "email": "john.doe@example.com", "firstname": "John", "lastname": "Doe", "disabled": false }, "_links": { "self": { "href": "http://hal-api.development/users/456", "templated": true }, "parent": { "href": "http://hal-api.development/users", "templated": false }, "users.posts": { "href": "http://hal-api.development/users/456/posts", "templated": true }, "users.update": { "href": "http://hal-api.development/users/456", "templated": true }, "users.destroy": { "href": "http://hal-api.development/users/456", "templated": true }, "users.posts": { "href": "http://hal-api.development/users/456/posts", "templated": true } }, "_embedded": { } } ] } }
贡献
随时欢迎贡献。首先查看有关包开发的Laravel 文档。一旦您做了些修改,请将它们推送到一个新分支,并开始发起一个拉取请求。
许可证
此项目是开源软件,根据MIT许可证授权。