jarischaefer/hal-api

通过自动化常见任务来增强您的HATEOAS体验。

v0.2 2017-04-19 19:30 UTC

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许可证授权。