laragear/api-manager

Manage multiple REST servers to make requests in few lines and fluently.

v2.0.0 2024-03-14 05:18 UTC

This package is auto-updated.

Last update: 2024-04-14 05:30:50 UTC


README

Latest Version on Packagist Latest stable test run Codecov coverage Maintainability Sonarcloud Status Laravel Octane Compatibility

Manage multiple REST servers to make requests in few lines and fluently. No more verbose HTTP Requests!

use App\Http\Apis\Chirper;

$chirp = Chirper::api()->chirp('Hello world!');

Become a sponsor

Your support allows me to keep this package free, up-to-date and maintainable. Alternatively, you can spread the word!

Requirements

  • Laravel 10 or later

Installation

Require this using Composer into your project:

composer require laragear/api-manager

Usage

Creating an API Server

To make use of an API server, define a class that extends Laragear\ApiManager\ApiServer. You may use the make:api Artisan command to make a ready-made stub in the app\Http\Apis directory.

php artisan make:api Chirper

You will receive a file with a base URL and actions, and space to add some headers and a bearer token. You're free to adjust it to your needs.

namespace App\Http\Apis;

use Laragear\ApiManager\ApiServer;

class Chirper extends ApiServer
{
    /**
     * The headers to include in each request.
     *
     * @var array{string:string}|array
     */
    public $headers = [
        // ...
    ];
    
    /**
     * The list of simple actions for this API.
     *
     * @var array|string[]
     */
    public $actions = [
        'latest' => '/',
        'create' => 'post:new',
    ];

    /**
     * Returns the API base URL.
     *
     * @return string
     */
    public function getBaseUrl()
    {
        return app()->isProduction()
            ? 'https://chirper.com/api/v1'
            : 'https://dev.chirper.com/api/v1';
    }
     
     /**
      * Returns the Bearer Token used for authentication. 
      * 
      * @return string
      */
     protected function authToken()
     {
         return config('services.chirper.secret');
     }
}

Note

You can override the API Server stub creating one in stubs/api.stub.

Inline Actions

Setting actions in the API class solves the problem of having multiple endpoints and preparing each one every time across your application, which can led to errors or convoluted functions full of text.

The easiest way to define actions is to use the $actions array using the syntax verb:route/{parameters}, being the key the action name you will invoke later. If you don't define a verb, get will be inferred.

/**
 * The list of simple actions for this API.
 * 
 * @var array|string[]  
 */
protected $actions = [
    'new chirp' => 'post:new',
    'latest'    => 'latest',
    'view'      => 'chirp/{id}',
    'edit'      => 'update:chirp/{id}',
    'delete'    => 'delete:chirp/{id}',
];

For example, to update a chirp, we could call edit directly from our ChirpApi.

While you're at it, add the PHPDoc manually to your API Server to take advantage of autocompletion (intellisense).

use Laragear\ApiManager\ApiServer;

/**
 * @method \Illuminate\Http\Client\Response newChirp($data = [])
 * @property-read \Illuminate\Http\Client\Response $latest
 * @property-read  \Illuminate\Http\Client\Response $view
 * @method \Illuminate\Http\Client\Response edit($data = [])
 */
class Chirper extends ApiServer
{
    // ...
}

Then, call the action name in camelCase notation. Arguments will be passed down to the HTTP Request.

use App\Http\Apis\Chirper;

// Create a new chirp.
$chirp = Chirper::api()->newChirp(['message' => 'This should be complex']);

If the route has named parameters, you can set them as arguments when invoking the server.

use App\Http\Apis\Chirper;

// Edit a chirp.
Chirper::api(['id' => 231])->edit(['message' => 'No, it was a breeze!']);

// Same as:
Chirper::api('chirper')->withUrlParameters(['id' => 231])->edit(['message' => 'No, it was a breeze!']);

Also, you can call an action without arguments as it were a property.

use App\Http\Apis\Chirper;

$latestChirps = Chirper::api()->latest;

Method actions

For more complex scenarios, you may use a class methods. Just be sure to type-hint the PendingRequest on any parameter if you need to customize the request.

use Illuminate\Http\Client\PendingRequest;

public function newChirp(PendingRequest $request, string $message)
{
    return $request->connectTimeout(10)->post('new', ['message' => $message]);
}

public function noReply(PendingRequest $request)
{
    $request->withHeaders(['X-No-Reply' => 'false'])
    
    return $this;
}

Then later, you can invoke the class methods like any monday morning.

use App\Http\Apis\Chirper;

$chirp = Chirper::api()->newChirp('Easy peasy');

Note

Method actions take precedence over inline actions.

As with inline actions, method actions can be also executed as it where properties if these don't require arguments.

use App\Http\Apis\Chirper;

$latest = Chirper::api()->noReply->newChirp('Easy peasy');

Authentication

An API Server supports the three types of authentication of the HTTP Client in Laravel: Basic, Digest and Bearer Token. You may define each of them as an array of username and password using authBasic() or authDigest(), and authToken() with the token, respectively.

/**
 * Returns the Basic Authentication to use against the API.
 * 
 * @var array{string:string}|void
 */
public function authBasic()
{
    return app()->isProduction()
        ? ['app@chirper.com', 'real-password']
        : ['dev@chirper.com', 'fake-password'];
}

Warning

Don't use an associative array to match the underlying methods. Since Laravel doesn't warranty consistency on named arguments, you should opt for simple arrays.

// This is supported, but discouraged!
return ['username' => 'app@chirper', 'password' => 'real-password'];

Before & After building a requests

You have the option to modify the request before and after it's bootstrapped using the beforeBuild() and afterBuild() respectively. The beforeBuild() is executed after the PendingRequest instance receives the base URL, and the afterBuild() is called after the headers and authentication are incorporated.

use Illuminate\Http\Client\PendingRequest;

public function beforeBuild(PendingRequest $request)
{
    //
}

public function afterBuild(PendingRequest $request)
{
    //
}

You're free here to tap into the request instance and modify it for all endpoints, or return an entirely new PendingRequest instance.

Tip

If you're using the old build() method from previous versions, it will still work since the beforeBuild() will call to build().

Overriding a request

The API request can be overridden as usual. All methods are passed down to the Illuminate\Http\Client\PendingRequest instance if these don't exist on the API Class.

use App\Http\Apis\Chirper;

$chirp = Chirper::api()->timeout(5)->latest();

Note

If the method exists in your API Class, it will take precedence.

Dependency Injection

All API Servers are resolved using the Service Container, so you can add any service you need to inject in your object through the constructor.

use Illuminate\Filesystem\Filesystem;
use Laragear\ApiManager\ApiServer;

class Chirper extends ApiServer
{
    public function __construct(protected Filesystem $file)
    {
        if ($this->file->missing('important_file.txt')) {
            throw new RuntimeException('Important file missing!')
        }
    }
    
    // ...
}

You can also create a callback to resolve your API Server in your AppServiceProvider if you need more deep customization to create it.

// app\Providers\AppServiceProvider.php
use App\Http\Apis\Chirper;

public function register()
{
    $this->app->bind(Chirper::class, function () {
       return new Chirper(config('services.chirper.version'));
    })
}

Concurrent Requests

To add an API Server Request to a pool, use the onPool() method for each concurrent request. There is no need to make all requests to the same API server, as you can mix and match different destinations.

use Illuminate\Support\Facades\Http;
use App\Http\Apis\Chirper;
use App\Http\Apis\Twitter;

$responses = Http::pool(fn ($pool) => [
    Chirper::api()->on($pool)->chirp('Hello world!'),
    Twitter::api()->on($pool)->tweet('Goodbye world!'),
    $pool->post('mastodon.org/api', ['message' => 'Greetings citizens!'])
]);
 
return $responses[0]->ok();

You may also name the requests using a second argument to on().

use Illuminate\Support\Facades\Http;
use App\Http\Apis\Chirper;
use App\Http\Apis\Twitter;

$responses = Http::pool(fn ($pool) => [
    Chirper::api()->on($pool, 'first')->chirp('Hello world!'),
    Twitter::api()->on($pool, 'second')->tweet('Goodbye world!'),
    $pool->as('third')->post('mastodon.org/api', ['message' => 'Greetings citizens!'])
]);
 
return $responses['first']->ok();

Wrapping into custom responses

You may find yourself receiving a response and having to map the data to your own class manually. Instead of juggling your way to do that, you can automatically wrap the incoming response into a custom "API Response".

First, create a custom response for an api using make:api-response, the API you want to use, and name the custom response with the same name of the endpoint. Ideally, you would want to name it the same as the action or method you plan to use it for.

php artisan make:api-response Chirper ViewResponse

You will receive a file like this:

namespace App\Http\Apis\Chirper\Responses;

use Illuminate\Http\Client\Response;

class ViewResponse extends Response
{
    //
}

Note

You can override the API Response stub creating one in stubs/api-response.stub.

In this class you can make any method you want. Since your class will extend the base Laravel HTTP Client Response class, you will have access to all its convenient methods.

public function isPrivate(): bool
{
    return $this->json('metadata.is_private', false)
}

Once you finish up customizing your custom API Response, you may map it to the actions and methods using the $responses array of your Api class.

/**
 * Actions and methods to wrap into a custom response class. 
 * 
 * @var array<string, class-string>  
 */
protected $responses = [
    'view' => Responses\ViewResponse::class,
];

This will enable the API Manager to wrap the response into your own every time you call that method to receive a response. For example, if you call view(), you will receive a new ViewResponse instance.

use App\Http\Apis\Chirper;

$chirp = Chirper::api(['id' => 5])->view();

if ($chirp->successful() && $chirp->isPrivate()) {
    return 'The chirp cannot be seen publicly.';
}

Tip

When using async() requests, custom responses are automatically wrapped once resolved.

Testing

You can easily test if an API Server action works or not by using the fake() method of the HTTP facade.

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Request;

public function test_creates_new_chirp(): void
{
    Http::fake(function (Request $request) {
        return Http::response([
            'posted' => 'ok', 
            ...json_decode($request->body())
        ], 200);
    });
    
    $this->post('spread-message', ['message' => 'Hello world!'])
        ->assertSee('Posted!');
}

Laravel Octane Compatibility

  • There are no singletons using a stale application instance.
  • There are no singletons using a stale config instance.
  • There are no singletons using a stale request instance.
  • There are no static properties written.

There should be no problems using this package with Laravel Octane.

Security

If you discover any security related issues, please email darkghosthunter@gmail.com instead of using the issue tracker.

License

This specific package version is licensed under the terms of the MIT License, at time of publishing.

Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2024 Laravel LLC.