All Projects → jarischaefer → hal-api

jarischaefer / hal-api

Licence: MIT License
Enhances your HATEOAS experience by automating common tasks.

Programming Languages

PHP
23972 projects - #3 most used programming language

Projects that are alternatives of or similar to hal-api

php-serializer
Serialize PHP variables, including objects, in any format. Support to unserialize it too.
Stars: ✭ 47 (+46.88%)
Mutual labels:  hal, hal-api
akka-http-hal
HAL (Hypermedia Application Language) specification support for akka-http
Stars: ✭ 18 (-43.75%)
Mutual labels:  hal, hateoas
react-ketting
Ketting bindings for React
Stars: ✭ 13 (-59.37%)
Mutual labels:  hateoas, hateoas-hal
RestWithASP-NETUdemy
No description or website provided.
Stars: ✭ 40 (+25%)
Mutual labels:  hateoas
express-hateoas-links
Extends express res.json to simplify building HATEOAS enabled REST API's
Stars: ✭ 26 (-18.75%)
Mutual labels:  hateoas
skynet robot control rtos ethercat
Realtime 6-axis robot controller, based on Qt C++ & OpenCascade & KDL kinematics & HAL
Stars: ✭ 41 (+28.13%)
Mutual labels:  hal
halfred
A parser for application/hal+json
Stars: ✭ 44 (+37.5%)
Mutual labels:  hal
jesi
Hypermedia API Accelerator
Stars: ✭ 19 (-40.62%)
Mutual labels:  hal
MPU6050
STM32 HAL library for GY-521 (MPU6050) with Kalman filter
Stars: ✭ 114 (+256.25%)
Mutual labels:  hal
bxcan
bxCAN peripheral driver for STM32 chips
Stars: ✭ 22 (-31.25%)
Mutual labels:  hal
laravel5-hal-json
Laravel 5 HAL+JSON API Transformer Package
Stars: ✭ 15 (-53.12%)
Mutual labels:  hal
riskfirst.hateoas
Powerful HATEOAS functionality for .NET web api
Stars: ✭ 69 (+115.63%)
Mutual labels:  hateoas
php-hal
HAL+JSON & HAL+XML API transformer outputting valid (PSR-7) API Responses.
Stars: ✭ 30 (-6.25%)
Mutual labels:  hal
hal-client
A lightweight client for consuming and manipulating Hypertext Application Language (HAL) resources.
Stars: ✭ 21 (-34.37%)
Mutual labels:  hal
STM32 HAL FREEMODBUS RTU
FreeMODBUS RTU port for STM32 HAL library
Stars: ✭ 111 (+246.88%)
Mutual labels:  hal
HX711
HX711 driver for STM32 HAL
Stars: ✭ 34 (+6.25%)
Mutual labels:  hal
HAL-Webinar
Webinar – Creating a Hardware Abstraction Layer in LabVIEW
Stars: ✭ 22 (-31.25%)
Mutual labels:  hal
zend-expressive-hal
Hypertext Application Language implementation for PHP and PSR-7
Stars: ✭ 37 (+15.63%)
Mutual labels:  hal
console
HAL management console
Stars: ✭ 41 (+28.13%)
Mutual labels:  hal
mezzio-hal
Hypertext Application Language implementation for PHP and PSR-7
Stars: ✭ 19 (-40.62%)
Mutual labels:  hal

HAL-API

Enhances your HATEOAS experience by automating common tasks.

About

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.

Installation

Requirements

Requires Laravel 5.4 and PHP 7.1.

Composer

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.

Service Provider

app.php

Register the Service Provider in your config/app.php file.

'providers' => [
	Jarischaefer\HalApi\Providers\HalApiServiceProvider::class,
]

compile.php (optional step)

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.

Usage

Simple Controller

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 Controller

Resource controllers require three additional components:

  • Model: Resources' data is contained within models
  • Repository: Repositories retrieve and store models
  • Transformer: Transforms models into HAL representations
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);
	}

}

Models

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);
	}

}

Repository

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;
	}

}

Searchable repository

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 ['*'];
	}

}

Transformer

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,
		];
	}

}

Linking relationships

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": {
	}
}

Embedded relationships

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": "[email protected]",
				"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": {
			}
		}
	}
}

Dependency wiring

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);
		});
	}

}

routes.php

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();

Disabling CRUD operations and pagination

RouteHelper::make($router)
	->resource('users', UsersController::class, [RouteHelper::SHOW, RouteHelper::INDEX], false)
	->done();

Searching/filtering

The controller's repository must implement HalApiSearchRepository.

RouteHelper::make($router)
	->resource('users', UsersController::class)
		->searchable()
	->done();

RouteServiceProvider

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);
}

Exception handler

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);
		}
	}

}

Examples

JSON for a specific model (show)

{
	"data": {
		"id": 123,
		"username": "FB",
		"email": "[email protected]",
		"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 for a list of models (index)

{
	"_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": "[email protected]",
					"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": "[email protected]",
					"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": {
				}
			}
		]
	}
}

Contributing

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.

License

This project is open-sourced software licensed under the MIT license.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].