API Client is a Data Access Object targeting API's.

0.2.1 2019-03-26 10:04 UTC

This package is auto-updated.

Last update: 2021-03-26 14:50:00 UTC


README

pipeline status

API Client is a Data Access Object targeting API's.

Requirements

The following versions of PHP are supported.

  • PHP 7.2

Installation

To install, use composer:

composer require wittestier/apic

Implementation

APIC contains a collection of interfaces, these interfaces guide you to setup a DOA for any web API.

Before you can use APIC, you'll need to create some classes, a working API where you have access to will be a good idea.

Model

Models or entities are Plain Old Php Objects or popo's. A models require one or more properties of any visibility. These models will be populated with the data, fetched from the API.

<?php

declare(strict_types=1);

namespace Api\Model;

class Book
{
    private $id;
    private $isbn;
    private $description;
    private $author;
    private $title;
    private $publicationDate;
}

Api is the namespace in these examples. You can use any namespace you want. Actually the model is your class. It can even be the same model as you already use with your ORM.

Gateway

Next, you'll need to create a Gateway class. A gateway is "an object that encapsulates access to an external system or resource" - P of EEA. With other words, the gateway is responsible for the communication with the API server.

For the sake of this example we use Guzzle as HTTP library. You're free to use any HTTP library you like.

Your Gateway class must implement the Apic\Gateway\GatewayInterface interface.

<?php

declare(strict_types=1);

namespace Api;

use Apic\Criteria\CriteriaInterface;
use Apic\Gateway\GatewayInterface;
use GuzzleHttp\Client;

class Gateway implements GatewayInterface
{
    private $client;


    public function __construct(Client $client)
    {
        $this->client = $client;
    }


    public function create(array $data, CriteriaInterface $criteria): array
    {
        return $this->request('POST', $criteria->toString(), $data);
    }

    public function delete(array $data, CriteriaInterface $criteria): void
    {
        $data = null;

        $this->request('DELETE', $criteria->toString(), $data);
    }

    public function retrieve(CriteriaInterface $criteria): array
    {
        return $this->request('GET', $criteria->toString(), null);
    }

    public function update(array $data, CriteriaInterface $criteria): array
    {
        return $this->request('PUT', $criteria->toString(), $data);
    }

    private function request(string $method, string $uri, ?array $payload): array
    {
        $options = [];
        if (empty($payload) === false) {
            $options['json'] = \GuzzleHttp\json_encode($payload);
        }

        $response = $this->client->request($method, $uri, $options);
        $result = (string)$response->getBody();

        return \GuzzleHttp\json_decode($result, true);
    }
}

The create method is called when a new object is added to a repository, update is called when an already existing object is added to a repository.

The delete method is called when an object is removed from a repository.

And the retrieve method is called when a repository searches for one or more objects.

Criteria

As you can see, all methods in the gateway accept a Criteria object. A criteria is "an object that represents a database query" - P of EAA. In the context of an API, a criteria object can be used to define query, ordering, sorting, pagination, filtering, searching or any other logic you need.

Your Criteria class must implement the Apic\Criteria\CriteriaInterface interface.

<?php

declare(strict_types=1);

namespace Api;

use Apic\Criteria\CriteriaInterface;

class Criteria implements CriteriaInterface
{
    private $path;
    private $id;


    public function __construct(string $path, ?int $id)
    {
        $this->path = $path;
        $this->id = $id;
    }


    public function toString(): string
    {
        return \rtrim(join('/', [$this->path, $this->id]), '/');
    }
}

Repository

A repository "mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects." - P of EAA.

You'll need to create a repository class for each end-point in the API. You can also say that you need to create a repository for each model. A repository class should implement methods that make sense to your business logic.

Your Repository class must implement the Apic\Repository\RepositoryInterface interface or extend the Apic\Repository\AbstractRepository class.

<?php

declare(strict_types=1);

namespace Api\Repository;

use Api\Criteria;
use Api\Model\Book;
use Api\Parser\BookInputParser;
use Api\Parser\BookListParser;
use Api\Parser\BookOutputParser;
use Apic\Repository\AbstractRepository;

class BookRepository extends AbstractRepository
{
    protected function getIdProperty(): string
    {
        return 'id';
    }


    public function findFirstPage(): array
    {
        return $this->findAll(
            Book::class,
            new Criteria('/books', null),
            new BookListParser()
        );
    }

    public function findById(int $id): ?Book
    {
        return $this->find(
            Book::class, 
            new Criteria('/books', $id), 
            new BookInputParser()
        );
    }


    public function persist(Book $book): Book
    {
        return $this->add(
            $book, 
            new Criteria('/books', $book->getId()), 
            new BookInputParser(), 
            new BookOutputParser()
        );
    }

    public function delete(Book $book): void
    {
        $this->remove(
            $book,
            new Criteria('/books', $book->getId()),
            new BookOutputParser()
        );
    }
}

A custom repository method can be as simple as creating an Criteria object and call the underlying add, find, findAll or remove methods.

Parser

A parser is an object that parse the API response payload into usable data. Parsers can be used to change the data structure, change value type or anything you need to do with the data before it is populated into models or sent to the API.

<?php

declare(strict_types=1);

namespace Api\Parser;

use Apic\Parser\ParserInterface;

class BookInputParser implements ParserInterface
{
    public function parse(array $data): array
    {
        if (isset($data['publicationDate']) === true) {
            $data['publicationDate'] = new \DateTime($data['publicationDate']);
        }

        return $data;
    }
}
<?php

declare(strict_types=1);

namespace Api\Parser;

use Apic\Parser\ParserInterface;

class BookOutputParser implements ParserInterface
{
    public function parse(array $data): array
    {
        if (isset($data['publicationDate']) === true
            && is_a($data['publicationDate'], \DateTime::class) === true
        ) {
            /** @var \DateTime $date */
            $date = $data['publicationDate'];

            $data['publicationDate'] = $date->format('c');
        }

        return $data;
    }
}
<?php

declare(strict_types=1);

namespace Api\Parser;

use Apic\Parser\ParserInterface;

class BookListParser implements ParserInterface
{
    public function parse(array $data): array
    {
        $bookParser = new BookInputParser();

        return array_map(function ($data) use ($bookParser) {
            return $bookParser->parse($data);
        }, $data['hydra:member']);
    }
}

An InputParser is a parser that parses the payload from the API so it can be used in you application.

An OutputParser is a parser that parses the data extracted from your models so it can be sent to the API.

Client

<?php

$httpClient = new \GuzzleHttp\Client([
    'base_uri' => 'https://demo.api-platform.com',
]);
$gateway = new \Api\Gateway($httpClient);
$client = new \Apic\Client($gateway, new \Zend\Hydrator\Reflection());
<?php

$bookRepository = $client->createRepository(\Api\Repository\BookRepository::class);


// Fetch the first page.
$books = $bookRepository->findFirstPage();

echo '##### Find first 30 books #####' . PHP_EOL;
var_dump($books);
echo '##### end #####' . PHP_EOL;


// Fetch by id
$book = $bookRepository->findById(3);

echo '##### Find book with ID 3 #####' . PHP_EOL;
var_dump($book);
echo '##### end #####' . PHP_EOL;


// Update book
$book = $bookRepository->findById(1);
$book->setAuthor('WitteStier');

$book = $bookRepository->persist($book);

echo '##### Updated book #####' . PHP_EOL;
var_dump($book);
echo '##### end #####' . PHP_EOL;


// Create new book
$book = new \Api\Model\Book();
$book->setIsbn('0132350882');
$book->setAuthor('WitteStier');
$book->setDescription('Test APIC documentation');
$book->setTitle('Test APIC');
$book->setPublicationDate(new \DateTime('now'));

$book = $bookRepository->persist($book);

echo '##### Create book #####' . PHP_EOL;
var_dump($book);
echo '##### end #####' . PHP_EOL;

License

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