appkr/api

RESTful HTTP API dev tool for Laravel or Lumen based project

v3.0.4 2021-04-12 08:15 UTC

README

Latest Stable Version Total Downloads Latest Unstable Version License

한국어 매뉴얼

INDEX

1. ABOUT

A lightweight RESTful API builder for Laravel or/and Lumen project.

2. FEATURE

  1. Provides Laravel/Lumen Service Provider for the league/fractal.
  2. Provides configuration capability for the library.
  3. Provides easy way of making transformed/serialized API response.
  4. Provides make:transformer artisan command.
  5. Provides examples, so that users can quickly copy & paste into his/her project.

3. LARAVEL/LUMEN IMPLEMENTATION EXAMPLE(How to use)

3.1. API Endpoint

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

3.2. Controller

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

4. HOW TO INSTALL

4.1. Composer.

$ composer require "appkr/api: 1.*"

4.2. Add the service provider.

<?php // config/app.php (Laravel)

'providers' => [
    Appkr\Api\ApiServiceProvider::class,
];
<?php // boostrap/app.php (Lumen)

$app->register(Appkr\Api\ApiServiceProvider::class);

4.3. [OPTIONAL] Publish assets.

# 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 !

5. CONFIG

Skim through the config/api.php, which is inline documented.

6. TRANSFORMER

6.1. What?

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).

6.2. Transformer Boilerplate Generator

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

7. NESTING SUB-RESOURCES

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

8. APIs

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.

8.1. 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);

8.2. 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)

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

9. BUNDLED EXAMPLE

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.

9.1. Activate examples

Uncomment the line.

<?php // vendor/appkr/api/src/ApiServiceProvider.php

$this->publishExamples();

9.2. Migrate and seed tables

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"

9.3. See it works

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).

9.4. [OPTIONAL] Run integration test

# 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.

10. LICENSE & CONTRIBUTION

MIT License. Issues and PRs are always welcomed.

11. CHANGELOG

v3.0.1

  • Supports auto package discovery in Laravel 5.5 (No need to add ServiceProvider in config/app.php)

v3.0.0

  • API not changed.
  • Update league/fractal version to 0.16.0

v2.3.4

  • jsonEncodeOption config added.

v2.3.3

  • Appkr\Api\Http\UnexpectedIncludesParamException will be thrown instead of UnexpectedValueException when includes query params are not valid.

v2.3.0

  • withCollection() now accepts Illuminate\Support\Collection.
  • Fix bug at SimpleArrayTransformer.

v2.2.0

  • 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')).

v2.1.0

  • TransformerAbstract::buildPayload method added to filter the list of response fields (Backward compatible).
  • Artisan generated transformer template changed (Backward compatible).

v2.0.0

  • 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.

v1.1.0 [buggy]

  • Field grouping feature added for partial response conveniences.
  • TransformerAbstract now throws UnexpectedValueException instead of Exception, when params or values passed by API client are not acceptable.

12. SPONSORS

  • Thanks JetBrains for supporting phpStorm IDE.