kix / apiranha
An API client for PHP
Requires
- php: ^7.1
- doctrine/annotations: @stable
- guzzlehttp/guzzle: ^6.2
- symfony/property-access: @stable
Requires (Dev)
- guzzlehttp/guzzle: @stable
- jms/serializer: @stable
- nikic/php-parser: @stable
- phpunit/phpunit: ^7.1
- sstalle/php7cc: ^1.1
- symfony/console: ^3.1
- symfony/serializer: @stable
Suggests
- guzzlehttp/guzzle: An HTTP client that's supported out-of-the-box
- jms/serializer: Adds support for (de)serializing common formats
- nikic/php-parser: Allows dumping definitions to PHP code
- symfony/serializer: Adds support for (de)serializing common formats
This package is auto-updated.
Last update: 2024-12-14 03:18:46 UTC
README
Apiranha is a library that makes consuming APIs easier and faster. Some of the inspiration for Apiranha comes from Retrofit
Example usage
A complete example can be found at examples/
. See ExampleCommand
and BuilderExampleCommand
.
Quick start
Make sure you have Composer installed, then run this in your project folder:
composer require kix/apiranha='dev-master'
Also, you'll need to require guzzlehttp/guzzle
if you don't want to bring your own implementation of an HTTP client.
In order to consume Github API, for example, first you need to declare your endpoints
in an interface and annotate those. The annotations available under the Kix\Apiranha\Annotation
namespace are:
- HTTP/REST method annotations:
Get
CGet
Delete
Post
Put
Returns
, a special annotation to denote the endpoint return type.
First, you need to decide which data you actually need from the endpoint. In our case, we're just getting the ID, name, language and stargazers count from the Github REST API:
namespace \Kix\Apiranha\Examples\Model; class Repository { private $id; private $name; private $language; private $stargazersCount; }
Of course, it will be necessary for you to read the properties, so you could either declare the fields as public
, or
you could add getters for the fields.
Next, to enable getting a single repo from the Github API, you would declare an interface like this (note the annotations):
use Kix\Apiranha\Annotation as Rest; interface GithubApi { /** * @Rest\Returns("\Kix\Apiranha\Examples\Model\Repository") * @Rest\Get("/repos/{username}/{repo}") */ public function getRepo(string $username, string $repo); }
Here, we say that the getRepo
method will be returning data from the /repos
endpoint.
Note that the URL parameters are consistently named and also typehinted with string
: this will help us generate a
correct URL for a specific call.
After you've implemented your endpoint interface and your model, it's time for the magic to happen:
use Kix\Apiranha\Builder; use Kix\Apiranha\Examples\Definition\GithubApi; /** @var $endpoint GithubApi */ $endpoint = Builder::createEndpoint('http://api.github.com', [GithubApi::class]);
Calling Builder::createEndpoint
, you get back an object that represents your API in terms of the interface you have
declared previously. Which, in turn, means you can annotate it with @var $endpoint GithubApi
.
Since now, all the methods you have declared in the interface become available via the endpoint object. Here's how it looks:
> $endpoint->listRepos('kix'); => array(30) { => object(Kix\Apiranha\Examples\Model\Repository)#76 (4) { => ["id":"Kix\Apiranha\Examples\Model\Repository":private]=> => int(43456580) => ["name":"Kix\Apiranha\Examples\Model\Repository":private]=> => string(6) "apiranha" => ["language":"Kix\Apiranha\Examples\Model\Repository":private]=> => NULL => ["stargazersCount":"Kix\Apiranha\Examples\Model\Repository":private]=> => int(1) => } => ... => }
Digging deeper
Well, what happens under the hood? The Builder
you've used in this simple API client is just a facade that helps you do
stuff quick, and hides a lot of implementation details you might not actually care about. However, there's more to it
than just calling a method and getting a result back. Here's what the builder presumes:
- Your routes will always be declared in the annotations you provide, and the parameters will be bound to the URIs as-is;
- You will be using Guzzle as the HTTP client (which this package will suggest upon installation)
- The API serialization format will be JSON, handled by Symfony's serializer
- Model instances your API will return will be hydrated using PHP's reflection API (which could be costly)
If some of these statements do not comply with your requirements, you're always free to extend the logic behind Apiranha.
Builder under the hood
Let's take a look at Builder
's insides. Here's what the createEndpoint
facade looks like:
public static function createEndpoint($baseUrl, array $definitions, array $listeners = array(), HttpAdapterInterface $adapter = null, Router $router = null) { if (!$adapter) { $adapter = new GuzzleHttpAdapter(new Client()); } if (!$router) { $router = new Router(); } if (!count($listeners)) { $serializerAdapter = new SymfonySerializerAdapter( new Serializer([], [new JsonEncoder()]) ); $serializerAdapter->addContentType('application/json', 'json'); $listeners[Endpoint::LISTENER_AFTER_RESPONSE] = new ContentTypeListenener($serializerAdapter); $listeners[Endpoint::LISTENER_AFTER_DATA] = new ReflectionHydratorListener(); } $endpoint = new Endpoint($adapter, $router, $baseUrl); foreach ($listeners as $evt => $listener) { $endpoint->addListener($evt, $listener); } $driver = new AnnotationDriver(); foreach ($definitions as $interfaceName) { $resources = $driver->createDefinitions($interfaceName); foreach ($resources as $resource) { $endpoint->addResourceDefinition($resource); } } return $endpoint; }
Note that you still can pass an array of listeners, an HTTP adapter and a router as arguments to this factory method. However, if you want to do things manually, or you dislike when things are decided for you (or, you just hate static facades), you could always reimplement this logic manually. That's all there is, basically.
Now, to the specifics. Why do we need all these things the Builder
instantiates for us?
HTTP layer
First of all, in order to consume an API, you need to somehow interact with the 3rd party server. For this, you need an
HTTP adapter. It should implement Kix\Apiranha\HttpAdapter\HttpAdapterInterface
, and GuzzleHttpAdapter
is an example
you can use right now.
The only method you have to implement is send(RequestInterface $request): ResponseInterface
, where RequestInterface
and
ResponseInterface
are standard Psr\Http
messages.
Resource definitions
When processing your annotated interface you've created before, the endpoint registers it as a resource. A resource is
an instance of Kix\Apiranha\ResourceDefinitionInterface
, which basically contains all the data necessary to make an
HTTP request to an API. You might think of it like a Swagger definition.
Router
A router is responsible for generating concrete URIs when given a resource and an array of parameters the request is executed with. The built-in router allows you to pass extra parameters you have not explicitly declared in a resource's path. Those will be added as query parameters.
Listeners
You can attach your own or built-in listeners to the lifecycle, which consists of three events:
Endpoint::LISTENER_BEFORE_REQUEST
, used to modify the request before it has been sent,Endpoint::LISTENER_AFTER_RESPONSE
, which is used to attach serializers to the workflow,Endpoint::LISTENER_AFTER_DATA
, which is used to hydrate your model instances.
All of the listeners can be implemented as callables, and 'after response' and 'after data' listeners can also implement
Kix\Apiranha\Listener\AfterResponseListenerInterface
or Kix\Apiranha\Listener\AfterDataListenerInterface
.
BEFORE_REQUEST listeners
A listener that is attached to the Endpoint::LISTENER_BEFORE_REQUEST
receives just one argument: the ResponseInterface
instance that has been instantiated for the current request. For example, you could use it to pass authorization headers
to a secured API:
$endpoint = Builder::createEndpoint('http://api.github.com', [GithubApi::class]); $endpoint->addListener(Endpoint::LISTENER_BEFORE_REQUEST, function (RequestInterface $request) { return $request->withAddedHeader('Authorization', 'Basic 12345'); });
AFTER_RESPONSE listeners
A listener attached to Endpoint::LISTENER_AFTER_RESPONSE
could either be a callable that matches the signature of
function(RequestInterface $request, ResponseInterface $response)
or an instance of
Kix\Apiranha\Listener\AfterResponseListenerInterface
. For example, such a listener could be helpful in case when the
HTTP client you are using does not throw for bad HTTP status codes.
Here's StatusCodeListener
, for example:
class StatusCodeListener implements AfterResponseListenerInterface { /** * @param RequestInterface $request * @param ResponseInterface $response * @throws \Exception * @return void */ public function process(RequestInterface $request, ResponseInterface $response) { if ($response->getStatusCode() > 400) { throw new \RuntimeException(sprintf( 'Bad status code: %s', $response->getStatusCode() )); } } }
Having the request also available allows you to log precise request/response interactions, for example.
Also note that you're allowed to return an instance of ApiResponse
, which wraps the PSR response, but also has a data
property which you can manipulate.
AFTER_DATA listeners
A listener attached to Endpoint::LISTENER_AFTER_DATA
could either be a callable that matches the signature of
function(ResponseInterface $response, ResourceDefinitionInterface $resource)
or an instance of
Kix\Apiranha\Listener\AfterDataListenerInterface
.
Such listeners are used by the library to hydrate objects with data returned by the API we wrap.
Hydration
There are several options available for the hydration strategies:
- Hydration via reflection, implemented in
ReflectionHydratorListener
- Hydration with
ocramius/generated-hydrator
, implemented inGeneratedHydratorListener
Reflection hydrator is always available, as long as PHP has reflection APIs available. Generated hydrator is an alternative implementation which could be more performant.
Both of these strategies are implemented as listeners and can be attached to the Endpoint::LISTENER_AFTER_DATA
event:
$endpoint = new Endpoint($adapter, new Router(), 'http://api.github.com'); $endpoint->addListener(Endpoint::LISTENER_AFTER_DATA, new ReflectionHydratorListener());
TODO: benchmarks
Once the model instance has been hydrated, you get back an instance of the class you've declared as your model. And that's it!