Enhances your HATEOAS experience by automating common tasks.
This package is based on Laravel 5. It is designed to automate common tasks in RESTful API programming. These docs might not always be in sync with all the changes.
Requires Laravel 5.4 and PHP 7.1.
Either require the package via Composer by issuing the following command
composer require jarischaefer/hal-api:dev-master
or by including the following in your composer.json.
"require": {
"jarischaefer/hal-api": "dev-master"
}
Check the releases page for a list of available versions.
Register the Service Provider in your config/app.php file.
'providers' => [
Jarischaefer\HalApi\Providers\HalApiServiceProvider::class,
]
Register the Service Provider in your config/compile.php file.
'providers' => [
Jarischaefer\HalApi\Providers\HalApiServiceProvider::class,
]
Run php artisan optimize --force
to compile an optimized classloader.
This type of controller is not backed by a model and provides no CRUD operations. A typical use case is an entry point for the API. The following controller should be routed to the root of the API and lists all relationships.
class HomeController extends HalApiController
{
public function index(HalApiRequestParameters $parameters)
{
return $this->responseFactory->json($this->createResponse($parameters)->build());
}
}
Resource controllers require three additional components:
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);
}
}
The following is a simple relationship with two tables. User has a One-To-Many relationship with Post.
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);
}
}
You may create an Eloquent-compatible
repository by extending HalApiEloquentRepository
and implementing its getModelClass() method.
class UserRepository extends HalApiEloquentRepository
{
public static function getModelClass(): string
{
return User::class;
}
}
class PostRepository extends HalApiEloquentRepository
{
public static function getModelClass(): string
{
return Post::class;
}
}
Implementing HalApiSearchRepository enables searching/filtering by field. An Eloquent-compatible repository is available. Not restricting the searchable fields might result in information leakage.
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 ['*'];
}
}
Transformers provide an additional layer between your models and the controller. They help you create a HAL response for either a single item or a collection of items.
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,
];
}
}
Overriding a transformer's getLinks method allows you to link to related resources. Linking a Post to its User:
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),
];
}
}
Notice the "users.show" relation among the links.
{
"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": {
}
}
Once data from two separate Models needs to be combined, the linking-approach doesn't quite cut it. Displaying Posts' authors (firstname and lastname in User model) becomes infeasible with more than a dozen items (N+1 GET requests to all "users.show" relationships). Embedding related data is basically the same as eager loading.
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),
];
}
}
Notice the "users.show" relation in the _embedded
field.
{
"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": {
}
}
}
}
It is recommended that you wire the transformers' dependencies in a Service Provider:
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);
});
}
}
The RouteHelper automatically creates routes for all CRUD operations.
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();
RouteHelper::make($router)
->resource('users', UsersController::class, [RouteHelper::SHOW, RouteHelper::INDEX], false)
->done();
The controller's repository must implement HalApiSearchRepository.
RouteHelper::make($router)
->resource('users', UsersController::class)
->searchable()
->done();
Make sure you bind all route parameters in the RouteServiceProvider. The callback shown below handles missing parameters depending on the request method. For instance, a GET request for a nonexistent database record should yield a 404 response. The same is true for all other HTTP methods except for PUT. PUT simply creates the resource if it did not exist before.
public function boot(Router $router)
{
parent::boot($router);
$callback = RouteHelper::getModelBindingCallback();
$router->model('users', User::class, $callback);
$router->model('posts', Post::class, $callback);
}
The callback above throws NotFoundHttpException if no record was found. To create a proper response instead of an error page, the exception handler must be amended. As shown below, various HTTP status codes like 404 and 422 will be returned depending on the exception caught.
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);
}
}
}
{
"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": {
}
}
{
"_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": {
}
}
]
}
}
Feel free to contribute anytime. Take a look at the Laravel Docs regarding package development first. Once you've made some changes, push them to a new branch and start a pull request.
This project is open-sourced software licensed under the MIT license.