bognerf/rest-grabber

Consume REST APIs

v2.3.0 2021-12-03 09:25 UTC

README

Coverage Latest Stable Version Total Downloads License

Work with REST APIs and assure the presence and validity of certain elements within the JSON response.

Table of Contents

Via Composer

$ composer require bognerf/rest-grabber

Usage

Basic usage without any validation or generation of derived objects (ValueObjects) is quite straightforward:

use Bognerf\RestGrabber\Grabber;
use Bognerf\RestGrabber\Url;

$url = new Url('https://reqres.in/api/unknown/2');
$grabber = new Grabber($url);
$contents = $grabber->get()->grab()->data();

You'll get an array according to the JSON provided by the response of https://reqres.in/api/unknown/2;

Global validation Rules

You can pass an array with dot notated keys to define obligatory values in the JSON response. Additionally, you can pass an array of constraints the value of the given key should match. RestGrabber relies on Symfony's Constraint library.

use Bognerf\RestGrabber\Grabber;
use Bognerf\RestGrabber\Url;

$url = new Url('https://reqres.in/api/users/2');
$grabber = new Grabber($url);
$grabber->getHandler()->setObligatoryFields([
    'data' => [
        new \Symfony\Component\Validator\Constraints\Count(['min'=>5])
    ],
    'data.email' => [
      new \Symfony\Component\Validator\Constraints\Email()
    ]
]);
$grabber->get()->grab();

The example above would query https://reqres.in/api/users/2 and make sure that the root level element data contains at least five sub-elements. Further, as denoted by data.email an element email must be present under data, which syntactically must conform to an email address.

ValueObjects

RestGrabber generates ValueObjects from certain sub-elements in our JSON (possibly even from the root element, if you so wish). For this to work, we must pass a Handler to our RestGrabber instance. By default, the library provides a generic Handler Bognerf\RestGrabber\Handlers\PlainJson. All handlers must extend the abstract class Bognerf\RestGrabber\Handler.

How to use them

Consider the following JSON example 1 (taken from https://reqres.in/api/unknown/):

{
  "page": 1,
  "per_page": 3,
  "total": 12,
  "total_pages": 4,
  "data": [
    {
      "id": 1,
      "name": "cerulean",
      "year": 2000,
      "color": "#98B2D1",
      "pantone_value": "15-4020"
    },
    {
      "id": 2,
      "name": "fuchsia rose",
      "year": 2001,
      "color": "#C74375",
      "pantone_value": "17-2031"
    },
    {
      "id": 3,
      "name": "true red",
      "year": 2002,
      "color": "#BF1932",
      "pantone_value": "19-1664"
    }
  ]
}

There are three objects listed under data. With RestGrabber you can define data as a ValueObject root element (deeper nesting is also possible using dot notation, e.g. data.sub-element), to automatically parse them and create a ValueObject from each of the three entries. Let's have a look:

use Bognerf\RestGrabber\Grabber;
use Bognerf\RestGrabber\Url;

$url = new Url('https://reqres.in/api/unknown');
$grabber = new Grabber($url);

$grabber->getHandler()->addValueObjectsRoot([
    'data' => []
]);
$grabber->get()->grab();
$valueObjects = $grabber->valueObjects('data');

We rely on the built-in Handler PlainJson. $valueObjects is now an array of three ValueObjects, which could be used like so:

foreach ($valueObjects as $vo) {
    echo $vo->getName() . PHP_EOL;
}

getName() is a magic getter function. You could retrieve any element of a ValueObject by get plus uppercased first letter of the element.

Additionally, usual magic getters are also available:

foreach ($valueObjects as $vo) {
    echo $vo->color . PHP_EOL;
}
JSON example 2

You aren't limited to only a single ValueObject root, there can be more. Consider our now slightly changed JSON:

{
  "page": 1,
  "per_page": 3,
  "total": 12,
  "total_pages": 4,
  "data": [
    {
      "id": 1,
      "name": "cerulean",
      "year": 2000,
      "color": "#98B2D1",
      "pantone_value": "15-4020"
    },
    {
      "id": 2,
      "name": "fuchsia rose",
      "year": 2001,
      "color": "#C74375",
      "pantone_value": "17-2031"
    },
    {
      "id": 3,
      "name": "true red",
      "year": 2002,
      "color": "#BF1932",
      "pantone_value": "19-1664"
    }
  ],
  "more-data": [
    {
    "id": 1,
    "name": "Florian"
    },
    {
    "id": 2,
    "name": "Katharina"
    }
  ]
}

As you can see, we have more than one potential ValueObject roots, namely data and more-data. Simply add more-data as a second ValueObject root:

use Bognerf\RestGrabber\Grabber;
use Bognerf\RestGrabber\Url;

$url = new Url('https://reqres.in/api/unknown');
$grabber = new Grabber($url);

$grabber->getHandler()->addValueObjectsRoot([
    'data' => []
]);

$grabber->getHandler()->addValueObjectsRoot([
    'more-data' => []
]);

$grabber->get()->grab();
$valueObjectsData = $grabber->getHandler()->valueObjects('data');
$valueObjectsMoreData = $grabber->getHandler()->valueObjects('more-data');

ValueObjects validation rules

Beyond defining obligatory values on a global scale for the whole JSON, you can also define validators for elements in each ValueObject root. Working again with our REST API at https://reqres.in/api/unknown , a root level element data having several objects could be defined as the ValueObject's root level. Remember, the root of ValueObjects is also made in dot notation. Let's see how to achieve this:

use Bognerf\RestGrabber\Grabber;
use Bognerf\RestGrabber\Url;

$url = new Url('https://reqres.in/api/unknown');
$grabber = new Grabber($url);

$grabber->getHandler()->addValueObjectsRoot([
    'data' => [
        'id' => [],
        'name' => [
            new \Symfony\Component\Validator\Constraints\Length(['min' => 3, 'max' => 128])
        ],
        'pantone_value' => [
            new \Symfony\Component\Validator\Constraints\Regex([
                'pattern' => '/^([0-9]){2}\-([0-9]){4}$/'
            ])
        ]
    ]
]);
$grabber->get()->grab();
$valueObjects = $grabber->valueObjects('data');

The example above would query https://reqres.in/api/unknown and validate that the root level element data exists and contains at least two elements. The root for our ValueObjects will also be set to data. Within data, each element must have an element id without further constraints, and an element name which should be at least three and at most 100 characters long. Finally, $grabber->valueObjects('data) will return an array of ValueObjects, each of which was checked to fulfill our constraints.

Custom handlers

For certain use cases it'll make sense to create a custom Handler. As an example, we'll use the API of Cloudflare's content delivery network cdnjs.com.

To ensure CDNJS's JSON response contains all critical elements we rely on, create your custom Handler like so (please don't forget to call parent::__construct() when extending the base Handler's constructor:

class CdnjsHandler extends \Bognerf\RestGrabber\Handler
{
    public function __construct()
    {
        parent::__construct();
        $this->obligatoryFields = [
            'assets' => [],
            'repository.type' => [
                new \Symfony\Component\Validator\Constraints\EqualTo(['value' => 'git']),
            ],
        ];

        $this->addValueObjectsRoot([
            'assets' => [
                'version' => [
                    new \Symfony\Component\Validator\Constraints\NotBlank(),
                ],
                'files' => [
                    new \Symfony\Component\Validator\Constraints\Count(['min' => 2])
                ]
            ]
        ]);
    }

}

The charming part is that the handler defines its own obligatory elements and validation rules. We're requiring the JSON response to contain an element assets as well as an element repository.type, which should have the value git. The latter being a great example for dot notated deeper level elements. Compare the actual JSON from CDNJS.

Furthermore, the collection or list under assets will consitute our single ValueObjects root, assuring that each and every one of it contains an element version (which must be present and not blank or empty or null) and an element files, which must be a collection with at least two elements.

Now let's put our new custom handler to service:

use Bognerf\RestGrabber\Grabber;
use Bognerf\RestGrabber\Url;

$url = new Url('https://api.cdnjs.com/libraries/axios');
$grabber = new Grabber($url);

$grabber->setHandler(new CdnjsHandler());

$grabber->get()->grab();
$valueObjects = $grabber->valueObjects('assets');

dump($valueObjects);

Working with Cache

RestGrabber can take a PSR-16 compatible CacheInterface via RestGrabber::setCache(). We'll use ZendCache, which is why we have to require some dependencies:

$ composer require zendframework/zend-cache
$ composer require zendframework/zend-serializer

But remember, you could use any PSR-16 compatible CacheInterface. It does not have to be Zend's.

$storage = StorageFactory::factory([
    'adapter' => [
        'name' => 'filesystem',
        'options' => [
            'cache_dir' => '/tmp/rest-grabber',
        ],
    ],
    'plugins' => [
        'serializer',
    ],
]);

$cache = new SimpleCacheDecorator($storage);

$url = new Url('https://api.cdnjs.com/libraries/axios');
$grabber = new Grabber($url);
$grabber->setCache($cache);
echo ($grabber->isCached() ? "Hit":"Missed");
$grabber->setHandler(new \RestGrabberTestbed\Handlers\CdnjsHandler());

$grabber->get()->grab();
$valueObjects = $grabber->valueObjects('assets');
echo ($grabber->isCached() ? "Hit":"Missed");

Testing

$ composer test

Security

If you discover any security related issues, please email fb@florianbogner.de instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.