TL;DR


No time? No problem... We provide expert level Laravel development and consulting. We also help customers scaling their projects. If you think we could fit together, drop us a message or give us a call. We are here to help.

Contact


+

APIs The Eloquent Way


Laravel solves almost all common use cases and provides best practices. Accessing data from your database? No problem, use eloquent! Validate user-data? Use FormRequests! Reading and writing data on AWS S3? Use flysystem!

Accessing third-party APIs? Use … wait a minute… Of course you can use Adam’s ZTTP Package or GuzzleHttp. But now you have to build all the logic for validation, transformation and so on yourself.

If only we could access APIs like we access the database. Isn’t there a better way? Let’s find out!

Motivation

Almost every project that we (as developers) work on nowadays has to integrate some third-party API. Whether you need a weather forecast for a specific date and location, the latest stock-market information, or INSERT_YOUR_USE_CASE_HERE, we have to interact with all kinds of APIs. (What was the last one you used?)

Even though APIs in general work in similar ways (see common standards), every one of them will have a unique set of endpoints and every endpoint will have a unique data structure.

Some APIs provide handy SDKs, often in the language of your choice. Amazon’s aws-php-sdk for example, offers high level access via an easy to use composer package. Unfortunately, not all APIs provide this luxury.

Think about the stock-market API from IEX. For quick access you can use ZTTP and simply request the list of available symbols in your controller.


$symbols = \Zttp\Zttp::get("https://api.iextrading.com/1.0/ref-data/symbols")->json();

If you want to do a little bit more, you would have to create an abstraction layer that handles errors, validation and transformation. The further you dig in, the more logic and abstraction should be placed in this service layer. Maybe you want to enforce a data structure or further filter your data in a way that is not provided by the API. Collections would come in handy here, right? Then in your next project, you have to use a different API. Most of the API-request handling is the same. Copy and Paste?

As developers, we want a more abstract, reusable solution. Something that is more like the way we access our database. An Eloquent model provides a set of reusable methods and a place for your own entity related code (attributes, relationships etc.). It also provides an elegant way to build queries through method chaining.


$user = User::where('email', $email)->first();

What if we could access an api like this?


$symbol = IEX::Symbol()->where('symbol', $apiSymbol)->first();

That got us thinking. Hang on.

Solution

Our goal was to provide an interface similar to eloquent, so we built a reusable abstraction layer that implements an “API, Endpoint, Shape” pattern.

The JSONPlaceholder API for example has a base_url “https://jsonplaceholder.typicode.com” and provides a “/posts” endpoint that returns data (userId, id, title, body) in a certain shape.

  • One API (JSONPlaceholder) is therefore an instance of ApiConsumer.
  • One Resource (Posts) is an instance of Endpoint.
  • The returned Post object is an instance of Shape.

Of course most APIs will have multiple Endpoints (and Shapes). The JSONPlaceholder API also provides a /comments Endpoint and a /users Endpoint, both with their own unique data-structure, or Shape. One ApiConsumer can have multiple Endpoints with Shapes.

The Endpoint offers the same method chaining capabilities as the Eloquent query builder and the Shape represents the data container of the Model.

Think about Endpoints as the place to tell your application how to query “all” users and how to “find” a specific user. Shapes represent the data-structure that is returned by an Endpoint.

To mimic eloquent even further, we want to return all data as a collection of their specific shape. That way a user can interact with that data, in the same native way he is used to with eloquent when accessing the database.

This is especially useful, when an API does not provide certain functionality, like filtering data for example. In laravel you can execute a “where”-clause through the query-builder and therefore on your database. Alternatively you can take advantage of the functionality that collections provide and execute a “filter”-method on a collection.


$users = User::where('age', '>=', 21)->get();


$users = User::all();
$users = $users->filter(function ($value, $key) {
    return $value->age >= 21
});

In Laravel, eloquent models also provide extended functionality. Think about transformations with mutators. Functionality that is separated from the database itself.


public function getFullNameAttribute()
{
    return "{$this->first_name} {$this->last_name}";
}

The Shapes also offer a place for custom code. Think about a data-structure for users that have two fields “first_name” and “last_name”. Instead of placing that into your controller or frontend code, you can add a function that handles that for you.

In the same way, we can specify the data-structure that we expect in a shape and apply a check, to see if the data returned by the api fulfills those structural requirements. We can also normalize the structure by applying a transformation like changing a field “username” to “user_name”.

What if you want to get the author (user) for a specific post? The basic structure of the API is similar to how you would setup your database-tables. The posts have a field user_id, that references the id in users.


$user = Post::first()->user;

We can mimic eloquents relations by utilizing custom implementations of hasOne() and hasMany() functions. That enables us to write clean queries, like this, for our JSONPlaceholder API:


$user = JSONPlaceholder::Post()->first()->user();

Results

Ok... so what do we have so far?

  • A Package black-bits/laravel-api-consumer
  • Artisan commands that generate the stubs for ApiConsumers, Endpoints and Shapes
  • Model resources for an api in Endpoints and define their structure in Shapes
  • A way to setup relations between resources (Endpoints)
  • Access to an API, the same way you access your database with eloquent
  • All collection methods chainable on Endpoints
  • Our own custom collection methods to use on Endpoints
  • Everything as a collection (Read HERE why collections are awesome)

  • BONUS - Caching out of the box

Disclaimer

Even though we are pretty proud about what we accomplished over the last weekend, this package is not ready for production yet. Feel free to give it a try though.

Let's checkout the showcase, shall we?

Show Case

Are you still with us? Ok good. It's more fun from here on. We built a show case website to play around with the package. You can download the show case project here, and the package is on packagist.org here, if you want to try it out yourself. (Design was obviously not our focus - Adam and Steve have awesome tutorials on Design)

  • We have implemented two simple APIs (IEX Trading API and JSONPlaceholder)
  • The IEX Trading API does not offer full REST capabilities. We make use of our CollectionCallbacks to filter/search Companies by Smybol or Name
  • JSON Placeholder API offers a straight forward REST API that returns demo data. We have implemented a where Method in our Endpoint
  • All the API related code is located in the app/ApiConsumers directory
  • The SymbolEndpoint has caching enabled (10 min)

ApiConsumers Setup

So what do i need to do to access the IEX API? Easy... just require our package and publish the configuration.


composer require black-bits/laravel-api-consumer

php artisan vendor:publish --provider="BlackBits\ApiConsumer\ApiConsumerServiceProvider"

Now, lets create our first ApiConsumer..


php artisan make:api-consumer IEX

... and our first Endpoint (the Shape will get created automatically)


php artisan make:api-consumer-endpoint Symbol -c IEX

Add the API Base URL to your configuration (/config/api-consumers.php)


return [
    'IEX' => [
        'apiBasePath' => 'https://api.iextrading.com/1.0',
    ],
];

Your previously created ApiConsumer Class should look like this (/app/ApiConsumers/IEX/IEX.php)


namespace App\ApiConsumers\IEX;

use BlackBits\ApiConsumer\ApiConsumer;

/**
 * Class IEX
 * @package  App\ApiConsumers\IEX
 */
class IEX extends ApiConsumer
{
    /**
     * @return  \Illuminate\Config\Repository|mixed
     */
    protected function getEndpoint()
    {
        return config('api-consumers.IEX.apiBasePath');
    }
}

Almost done ... let's define the resources in the previously created Symbol Endpoint (/app/ApiConsumers/IEX/Endpoints/SymbolEndpoint.php) $path specifies the url part on how to access the resource. With $shouldCache and $cacheDurationInMinutes you can define if API calls should be cached and for how long. The function all() defines how you expect to access all objects for that Endpoint.


namespace App\ApiConsumers\IEX\Endpoints;

use BlackBits\ApiConsumer\Support\Endpoint;

/**
 * Class SymbolEndpoint
 * @package  App\ApiConsumers\IEX\Endpoints
 */
class SymbolEndpoint extends Endpoint
{
    /**
     * @var  string
     */
    protected $path = '/ref-data/symbols';

    /**
     * @var  bool
     */
    protected $shouldCache = true;

    /**
     * @var  int
     */
    protected $cacheDurationInMinutes = 10;

    /**
     * @return  \Illuminate\Support\Collection|\Tightenco\Collect\Support\Collection
     * @throws  \Exception
     */
    public function all()
    {
        return $this->get();
    }
}

Technically we're good to go, but let's check the Shape (/app/ApiConsumers/IEX/Shapes/SymbolShape.php). Everything in here is optionally and the automatically created one is all you need. You can however set $return_shape_data_only if you only want to work with specific fields. You can also set $require_shape_structure to add a check if the structure of the api result is what you would expect. $transformations let's you normalize fields (iexId => iex_id) and function company() defines a relation.


namespace App\ApiConsumers\IEX\Shapes;

use App\ApiConsumers\IEX\Endpoints\CompanyEndpoint;
use BlackBits\ApiConsumer\Support\BaseShape;

/**
 * Class SymbolShape
 * @package  App\ApiConsumers\IEX\Shapes
 */
class SymbolShape extends BaseShape
{
    /**
     * @var  bool
     */
    protected $return_shape_data_only = true;

    /**
     * @var  bool
     */
    protected $require_shape_structure = true;

    /**
     * @var  array
     */
    protected $transformations = [
        'symbol'    => 'symbol',
        'name'      => 'name',
        'date'      => 'date',
        'isEnabled' => 'is_enabled',
        'type'      => 'type',
        'iexId'     => 'iex_id'
    ];

    /**
     * @var  array
     */
    protected $fields = [
        'symbol',
        'name',
        'date',
        'is_enabled',
        'type',
        'iex_id'
    ];

    /**
     * @return  mixed
     */
    public function company()
    {
        return $this->hasOne(CompanyEndpoint::class, 'symbol');
    }
}

Usage

From here on you can query all Symbols from the IEX API like this:


$symbols = IEX::Symbol()->all();

Let's say you want to get the related company information:


$symbols = IEX::Symbol()->all();
$company = $symbols->first()->company();

// or
$company = IEX::Symbol()->first()->company();

Ok, great ... remember that screenshot from the beginning? What code was necessary in the controller to make it work? Here it is:


/**
 * @return  \Illuminate\Http\Response
 */
public function list(Request $request)
{
    $symbols = IEX::Symbol();

    if (!empty($request->get('symbol'))) {
        $symbol = $request->get('symbol');
        $symbols->filter(function ($value, $key) use ($symbol) {
            return str_contains($value->symbol, $symbol);
        });
    }

    if (!empty($request->get('name'))) {
        $symbols->filterName($request->get('name'));
    }

    $data = [
        'symbols' => $symbols->take(10)->get()
    ];

    debug($data);
    return response()->view('symbols.list', $data);
}

Let's check out another example with the JSONPlaceholder API. Here we want to get a specific Post with all related Comments.

And the only code necessary in our controller is this. The ApiConsumer-, Endpoint- and Shape-Baseclasses take care of the rest. Caching, relations, transformations, validating and enforcing responses and so on.


/**
 * @return  \Illuminate\Http\Response
 */
public function listComments($post)
{
    $post = JSONPlaceholder::Post()->where('id', $post)->first();

    $data = [
        'comments' => $post->comments(),
        'post'     => $post
    ];

    debug($data);
    return response()->view('json-placeholder.comments-list', $data);
}

Feel free to checkout the code in the repository in detail and let us know what you think?

Package Deep Dive

Interested in a technical deep-dive on what is going on under the hood? Drop us a note at [email protected].

Next Steps

There is still a lot of work. Right now, there is no support for authentication. That is probably the next thing on our agenda. Afterwards we will implement other http methods (post / put /…) and improve the relation functionality. And of course ... testing.

Long term it could be possible to implement an auto-generation for standardized endpoints (JSON API / Swagger).

Conclusion

If you made it all the way down here, thanks you for your attention!

We certainly had a lot of fun going through ideas at the drawing board and exploring different implementations over the course of the weekend.

We will definitely pursue this further, as we believe that a standardized way to access APIs is still missing from the Laravel ecosystem.

We would love to get your input

  • What do you think about the code division into the Consumer -> Endpoint -> Shape structure?
  • Any important features we are missing?

  • What kind of APIs are you using the most?
  • What kind of APIs give you the most trouble implementing?

Expert Level Laravel Web Development & Consulting Agency

We love Laravel, and so should you. Let us show you why.

Find out about Laravel

About Us

About Us


Founded in 2014, Black Bits helps customers to reach their goals and to expand their market positions. We work with a wide range of companies, from startups that have a vision and first funding to get to market faster, to big industry leaders that want to profit from modern technologies. If you want to start on your project without building an internal dev-team first, or if you need extra expertise or resources, Black Bits - the Laravel Web Development Agency is here to help.

Laravel

Laravel is one of the most popular PHP Frameworks. Perfect for Sites and API's of all sizes. Clean, fast and reliable.

Lumen

Lumen is the perfect solution for building Laravel based micro-services and blazing fast APIs.

Vue.js

Live-Updating Dashboards, Components and Reactivity. Your project needs a modern user-interface? Vue.js is the right choice.

React

Live-Updating Dashboards, Components and Reactivity. Your project needs a modern user-interface? React is the right choice.

Bootstrap

Bootstrap is the second most-starred project on GitHub and the leading framework for designing modern front-ends.

TailwindCSS

Tailwind is a utility-first CSS framework for rapidly building custom user interfaces.

Amazon Web Services

Amazon Web Services (AWS) leads the worldwide cloud computing market. With a wide range of services, AWS is a perfect option for fast development.

Digital Ocean

Leave complex pricing structures behind. Always know what you'll pay per month. Service customers around the world from eight different locations.

Contact Us

Contact Us


You have the vision, we have the development expertise. We would love to hear about your project.

Send us a message or give us a call and see
how Black Bits can help you!

Grants Pass, Oregon, U.S.A.

phone: (+1) 541 592 9181

[email protected]