Package Data | |
---|---|
Maintainer Username: | appkr |
Maintainer Contact: | juwonkim@me.com (appkr) |
Package Create Date: | 2016-01-04 |
Package Last Update: | 2021-04-12 |
Language: | PHP |
License: | MIT |
Last Refreshed: | 2025-01-28 15:10:25 |
Package Statistics | |
---|---|
Total Downloads: | 14,948 |
Monthly Downloads: | 4 |
Daily Downloads: | 0 |
Total Stars: | 31 |
Total Watchers: | 5 |
Total Forks: | 6 |
Total Open Issues: | 0 |
A lightweight RESTful API builder for Laravel or/and Lumen project.
league/fractal
.make:transformer
artisan command.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
and Comment
relationship in the example, we provide true
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.
Appkr\Api\Response
- Available Methods<?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);
Appkr\Api\TransformerAbstract
- Available Methods<?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)
helpers.php
- Available Functions<?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:
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.
league/fractal
version to 0.16.0jsonEncodeOption
config added.Appkr\Api\Http\UnexpectedIncludesParamException
will be thrown instead of UnexpectedValueException
when includes query params are not valid.withCollection()
now accepts Illuminate\Support\Collection
.SimpleArrayTransformer
.config('api.convert.key')
).config('api.convert.date')
).TransformerAbstract::buildPayload
method added to filter the list of response fields (Backward compatible).TransformerAbstract
's API changed.$visible
or $hidden
property.TransformerAbstract
now throws UnexpectedValueException
instead of Exception
, when params or values passed by API client are not acceptable.