- 1. ABOUT
- 2. FEATURE
- 3. LARAVEL/LUMEN IMPLEMENTATION EXAMPLE
- 4. HOW TO INSTALL
- 5. CONFIG
- 6. TRANSFORMER
- 7. NESTING SUB-RESOURCE
- 8. APIs
- 9. BUNDLED EXAMPLE
-
- LICENSE & CONTRIBUTION
-
- CHANGELOG
A lightweight RESTful API builder for Laravel or/and Lumen project.
- Provides Laravel/Lumen Service Provider for the
league/fractal
. - Provides configuration capability for the library.
- Provides easy way of making transformed/serialized API response.
- Provides
make:transformer
artisan command. - Provides examples, so that users can quickly copy & paste into his/her project.
Define RESTful resource route in Laravel way.
<?php // app/Http/routes.php OR routes/web.php OR routes/api.php
Route::group(['prefix' => 'v1'], function () {
Route::resource(
'books',
'BooksController',
['except' => ['create', 'edit']]
);
});
Lumen doesn't support RESTful resource route. You have to define them one by one.
<?php // app/Http/routes.php OR routes/web.php OR routes/api.php
$app->group(['prefix' => 'v1'], function ($app) {
$app->get('books', [
'as' => 'v1.books.index',
'uses' => 'BooksController@index',
]);
$app->get('books/{id}', [
'as' => 'v1.books.show',
'uses' => 'BooksController@show',
]);
$app->post('books', [
'as' => 'v1.books.store',
'uses' => 'BooksController@store',
]);
$app->put('books/{id}, [
'as' => 'v1.books.update',
'uses' => 'BooksController@update',
]);
$app->delete('books/{id}', [
'as' => 'v1.books.destroy',
'uses' => 'BooksController@destroy',
]);
});
The subsequent code block is the controller logic for /v1/books/{id}
endpoint. Note the use cases of json()
helper and transformer on the following code block.
<?php // app/Http/Controllers/BooksController.php
namespace App\Http\Controllers\V1;
use App\Http\Controllers\Controller;
use App\Book;
use App\Transformers\BookTransformer;
use Illuminate\Http\Request;
class BooksController extends Controller
{
public function index()
{
return json()->withPagination(
Book::latest()->paginate(5),
new BookTransformer
);
}
public function store(Request $request)
{
// Assumes that validation is done at somewhere else
return json()->created(
$request->user()->create($request->all())
);
}
public function show($id)
{
return json()->withItem(
Book::findOrFail($id),
new BookTransformer
);
}
public function update(Request $request, $id)
{
$book = Book::findOrFail($id);
return ($book->update($request->all()))
? json()->success('Updated')
: json()->error('Failed to update');
}
public function destroy($id)
{
$book = Book::findOrFail($id);
return ($book->delete())
? json()->success('Deleted')
: json()->error('Failed to delete');
}
}
$ composer require "appkr/api: 1.*"
<?php // config/app.php (Laravel)
'providers' => [
Appkr\Api\ApiServiceProvider::class,
];
<?php // boostrap/app.php (Lumen)
$app->register(Appkr\Api\ApiServiceProvider::class);
# Laravel only
$ php artisan vendor:publish --provider="Appkr\Api\ApiServiceProvider"
The configuration file is located at config/api.php
.
In Lumen we can manually create config/api.php
file, and then activate the configuration at bootstrap/app.php
like the following.
<?php // bootstrap/app.php (Lumen)
$app->register(Appkr\Api\ApiServiceProvider::class);
$app->configure('api');
Done !
Skim through the config/api.php
, which is inline documented.
For more about what the transformer is, what you can do with this, and why it is required, see this page. 1 transformer for 1 model is a best practice(e.g. BookTransformer
for Book
model).
Luckily this package ships with an artisan command that conveniently generates a transformer class.
$ php artisan make:transformer {subject} {--includes=}
# e.g. php artisan make:transformer "App\Book" --includes="App\\User:author,App\\Comment:comments:true"
-
subject
_ The string name of the model class. -
includes
_ Sub-resources that is related to the subject model. By providing this option, your API client can have control over the response body. see NESTING SUB RESOURCES section.The option's signature is
--include=Model,eloquent_relationship_methods[,isCollection]
.If the include-able sub-resource is a type of collection, like
Book
andComment
relationship in the example, we providetrue
as the third value of the option.
Note
We should always use double back slashes (
\\
), when passing a namespace in artisan command WITHOUT quotation marks.$ php artisan make:transformer App\\Book --includes=App\\User:author,App\\Comment:comments:true
A generated file will look like this:
<?php // app/Transformers/BookTransformer.php
namespace App\Transformers;
use App\Book;
use Appkr\Api\TransformerAbstract;
use League\Fractal;
use League\Fractal\ParamBag;
class BookTransformer extends TransformerAbstract
{
/**
* List of resources possible to include using url query string.
*
* @var array
*/
protected $availableIncludes = [
'author',
'comments'
];
/**
* Transform single resource.
*
* @param \App\Book $book
* @return array
*/
public function transform(Book $book)
{
$payload = [
'id' => (int) $book->id,
// ...
'created' => $book->created_at->toIso8601String(),
'link' => [
'rel' => 'self',
'href' => route('api.v1.books.show', $book->id),
],
];
return $this->buildPayload($payload);
}
/**
* Include author.
* This method is used, when an API client request /v1/books?include=author
*
* @param \App\Book $book
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Item
*/
public function includeAuthor(Book $book, ParamBag $params = null)
{
return $this->item(
$book->author,
new \App\Transformers\UserTransformer($params)
);
}
/**
* Include comments.
* This method is used, when an API client request /v1/books??include=comments
*
* @param \App\Book $book
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Collection
*/
public function includeComments(Book $book, ParamBag $params = null)
{
$transformer = new \App\Transformers\CommentTransformer($params);
$comments = $book->comments()
->limit($transformer->getLimit())
->offset($transformer->getOffset())
->orderBy($transformer->getSortKey(), $transformer->getSortDirection())
->get();
return $this->collection($comments, $transformer);
}
}
An API client can request a resource with its sub-resource. The following example is requesting authors
list. At the same time, it requests each author's books
list. It also has additional parameters, which reads as 'I need total of 3 books for this author when ordered by recency without any skipping'.
GET /authors?include=books:limit(3|0):sort(id|desc)
When including multiple sub resources,
GET /authors?include[]=books:limit(2|0)&include[]=comments:sort(id|asc)
# or alternatively
GET /authors?include=books:limit(2|0),comments:sort(id|asc)
In case of deep recursive nesting, use dot (.
). In the following example, we assume the publisher model has relationship with somethingelse model.
GET /books?include=author,publisher.somethingelse
The following is the full list of response methods that Appkr\Api\Http\Response
provides. Really handy when making a json response in a controller.
<?php
// Generic response.
// If valid callback parameter is provided, jsonp response can be provided.
// This is a very base method. All other responses are utilizing this.
respond(array $payload);
// Respond collection of resources
// If $transformer is not given as the second argument,
// this class does its best to transform the payload to a simple array
withCollection(
\Illuminate\Database\Eloquent\Collection $collection,
\League\Fractal\TransformerAbstract|null $transformer,
string|null $resourceKey // for JsonApiSerializer only
);
// Respond single item
withItem(
\Illuminate\Database\Eloquent\Model $model,
\League\Fractal\TransformerAbstract|null $transformer,
string|null $resourceKey // for JsonApiSerializer only
);
// Respond collection of resources with pagination
withPagination(
\Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator,
\League\Fractal\TransformerAbstract|null $transformer,
string|null $resourceKey // for JsonApiSerializer only
);
// Respond json formatted success message
// api.php provides configuration capability
success(string|array $message);
// Respond 201
// If an Eloquent model is given at an argument,
// the class tries its best to transform the model to a simple array
created(string|array|\Illuminate\Database\Eloquent\Model $primitive);
// Respond 204
noContent();
// Respond 304
notModified();
// Generic error response
// This is another base method. Every other error responses use this.
// If an instance of \Exception is given as an argument,
// this class does its best to properly format a message and status code
error(string|array|\Exception|null $message);
// Respond 401
// Note that this actually means unauthenticated
unauthorizedError(string|array|null $message);
// Respond 403
// Note that this actually means unauthorized
forbiddenError(string|array|null $message);
// Respond 404
notFoundError(string|array|null $message);
// Respond 405
notAllowedError(string|array|null $message);
// Respond 406
notAcceptableError(string|array|null $message);
// Respond 409
conflictError(string|array|null $message);
// Respond 410
goneError(string|array|null $message);
// Respond 422
unprocessableError(string|array|null $message);
// Respond 500
internalError(string|array|null $message);
// Set http status code
// This method is chain-able
setStatusCode(int $statusCode);
// Set http response header
// This method is chain-able
setHeaders(array $headers);
// Set additional meta data
// This method is chain-able
setMeta(array $meta);
<?php
// We can apply this method against an instantiated transformer,
// to get the parsed query parameters that belongs only to the current resource.
//
// e.g. GET /v1/author?include[]=books:limit(2|0)&include[]=comments:sort(id|asc)
// $transformer = new BookTransformer;
// $transformer->get();
// Will produce all parsed parameters:
// // [
// // 'limit' => 2 // if not given default value at config
// // 'offset' => 0 // if not given default value at config
// // 'sort' => 'created_at' // if given, given value
// // 'order' => 'desc' // if given, given value
// // ]
// Alternatively we can pass a key.
// $transformer->get('limit');
// Will produce limit parameter:
// // 2
get(string|null $key)
// Exactly does the same function as get.
// Was laid here, to enhance readability.
getParsedParams(string|null $key)
<?php
// Make JSON response
// Returns Appkr\Api\Http\Response object if no argument is given,
// from there you can chain any public apis that are listed above.
json(array|null $payload)
// Determine if the current framework is Laravel
is_laravel();
// Determine if the current framework is Lumen
is_lumen();
// Determine if the current request is for API endpoints, and expecting API response
is_api_request();
// Determine if the request is for update
is_update_request();
// Determine if the request is for delete
is_delete_request();
The package is bundled with a set of example that follows the best practices. It includes:
- Database migrations and seeder
- routes definition, Eloquent Model and corresponding Controller
- FormRequest (Laravel only)
- Transformer
- Integration Test
Follow the guide to activate and test the example.
Uncomment the line.
<?php // vendor/appkr/api/src/ApiServiceProvider.php
$this->publishExamples();
Do the following to make test table and seed test data. Highly recommend to use SQLite, to avoid polluting the main database of yours.
$ php artisan migrate --path="vendor/appkr/api/src/example/database/migrations" --database="sqlite"
$ php artisan db:seed --class="Appkr\Api\Example\DatabaseSeeder" --database="sqlite"
Boot up a server.
$ php artisan serve
Head on to GET /v1/books
, and you should see a well formatted json response. Try each route to get accustomed to, such as /v1/books=include=authors
, /v1/authors=include=books:limit(2|0):order(id|desc)
.
# Laravel
$ vendor/bin/phpunit vendor/appkr/api/src/example/BookApiTestForLaravel.php
# Lumen
$ vendor/bin/phpunit vendor/appkr/api/src/example/BookApiTestForLumen.php
Note
If you finished evaluating the example, don't forget to rollback the migration and re-comment the unnecessary lines at
ApiServiceProvider
.
MIT License. Issues and PRs are always welcomed.
- Supports auto package discovery in Laravel 5.5 (No need to add ServiceProvider in config/app.php)
- API not changed.
- Update
league/fractal
version to 0.16.0
jsonEncodeOption
config added.
Appkr\Api\Http\UnexpectedIncludesParamException
will be thrown instead ofUnexpectedValueException
when includes query params are not valid.
withCollection()
now acceptsIlluminate\Support\Collection
.- Fix bug at
SimpleArrayTransformer
.
- Field name converting to snake or camel case depending on configuration (
config('api.convert.key')
). - Date format converting depending on configuration (
config('api.convert.date')
).
TransformerAbstract::buildPayload
method added to filter the list of response fields (Backward compatible).- Artisan generated transformer template changed (Backward compatible).
TransformerAbstract
's API changed.- Partial response by query string feature removed. Instead we can explicitly set the list of attributes to respond in a Transformer's
$visible
or$hidden
property.
- Field grouping feature added for partial response conveniences.
TransformerAbstract
now throwsUnexpectedValueException
instead ofException
, when params or values passed by API client are not acceptable.
- Thanks JetBrains for supporting phpStorm IDE.