Skip to content

Commit

Permalink
Merge pull request #6 from exonet/tsi-EXO-3827
Browse files Browse the repository at this point in the history
Add support for PATCH
  • Loading branch information
robbinjanssen committed Sep 6, 2019
2 parents 97fc6a7 + 125670d commit 382f43c
Show file tree
Hide file tree
Showing 16 changed files with 568 additions and 92 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
## Unreleased
[Compare v2.0.0 - Unreleased](https://github.com/exonet/exonet-api-php/compare/v2.0.0...master)

## [v2.1.0](https://github.com/exonet/exonet-api-php/releases/tag/v2.1.0) - 2019-09-06
[Compare v2.1.0 - v2.0.0](https://github.com/exonet/backend/compare/v2.0.0...v2.1.0)
### Added
- Support for patching resources and relationships.
- Exceptions thrown by the package are extended with the `status` as the exception code, the `code` as detailed code and an array containing the returned variables.

## [v2.0.0](https://github.com/exonet/exonet-api-php/releases/tag/v2.0.0) - 2019-07-02
[Compare v2.0.0 - v1.0.0](https://github.com/exonet/backend/compare/v1.0.0...v2.0.0)
## Breaking
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"require": {
"php": "~7.1",
"guzzlehttp/guzzle": "~6.0",
"psr/log": "^1.0"
"psr/log": "^1.0",
"ext-json": "*"
},
"require-dev": {
"mockery/mockery": "^1.0",
Expand Down
61 changes: 61 additions & 0 deletions docs/api_resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# API Resources

## Getting data from a resource
Get data from the attributes or relationships of a resource. See [making API calls](calls.md) for more information on
how to get resources from the API.

```php
$dnsRecord = $client->resource('dns_records')->get('VX09kwR3KxNo');

// Show an attribute value:
echo $dnsRecord->attribute('name');

// Get a related value, in this example the name of the DNS zone:
echo $dnsRecord->related('zone')->get()->attribute('name');
```

## Creating a new resource
Post a new resource to the API by setting its attributes and relationships:

```php
$record = new ApiResource('dns_records');
$record->attribute('name', 'www');
$record->attribute('type', 'A');
$record->attribute('content', '192.168.1.100');
$record->attribute('ttl', 3600);

// The value of a relationship must be defined as a resource identifier.
$record->relationship('zone', new ApiResourceIdentifier('dns_zones', 'VX09kwR3KxNo'));
$result = $record->post();

print_r($result);
```

## Modifying a resource
Modify a resource by changing its attributes and/or relationships:

```php
$dnsRecord = $client->resource('dns_records')->get('VX09kwR3KxNo');
// Or, if there is no need to retrieve the resource from the API first you can use the following:
// $dnsRecord = new ApiResource('dns_records', 'VX09kwR3KxNo');

// Change the 'name' attribute to 'changed-name'.
$dnsRecord->attribute('name', 'changed-name');

// The value of a relationship must be defined as a resource identifier.
$dnsRecord->relationship('dns_zone', new ApiResourceIdentifier('dns_zones', 'X09kwRdbbAxN'));

// Patch the changed data to the API.
$dnsRecord->patch();
```

## Deleting a resource
Delete a resource with a given ID:

```php
$dnsRecord = $client->resource('dns_records')->get('VX09kwR3KxNo');
// Or, if there is no need to retrieve the resource from the API first you can use the following:
// $dnsRecord = new ApiResource('dns_records', 'VX09kwR3KxNo');

$dnsRecord->delete();
```
6 changes: 3 additions & 3 deletions docs/api_responses.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Using API Responses
There are two types of API responses upon a successful request. If a single resource is requested then an `ApiResource` is
There are two types of API responses upon a successful request. If a single resource is requested then an [`ApiResource`](api_resources.md) is
returned, if multiple resources are requested then an `ApiResourceSet` is returned.

## The `ApiResourceSet` class
Expand All @@ -16,8 +16,8 @@ foreach ($certificates as $certificate) {
}
```

## The `ApiResource` class
Each resource returned by the API is transformed to an `ApiResource` instance. This makes it possible to have easy access
## The [`ApiResource`](api_resources.md) class
Each resource returned by the API is transformed to an [`ApiResource`](api_resources.md) instance. This makes it possible to have easy access
to the attributes, resourceType and ID of the resource. Each of these fields can be accessed as if it is a property on the class:

```php
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
- [The `ApiResourceSet` class](api_responses.md#the-apiresourceset-class)
- [The `ApiResource` class](api_responses.md#the-apiresource-class)
- [Relations](api_responses.md#relations)
- [API Resources](api_resources.md)

[Using this package »](using.md)
15 changes: 15 additions & 0 deletions examples/dns_record_post.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@

echo sprintf("\nNew DNS record has ID %s", $newRecord->id());

// Ask user if record should be updated.
echo sprintf("\n\nDo you want to patch this record? [y/n] ");
if ('Y' === strtoupper(trim(fgets(STDIN)))) {
$newRecord->attribute('content', 'Exonet API PATCH example '.microtime());
$newRecord->patch();
echo "\nDNS record patched:\n";
echo sprintf(
"%s\t%s\t%s\t%s\n",
$newRecord->attribute('type'),
$newRecord->attribute('fqdn'),
$newRecord->attribute('ttl'),
$newRecord->attribute('content')
);
}

// Ask user if record should be deleted.
echo sprintf("\nDo you want to delete this record? [y/n] ");
if ('Y' !== strtoupper(trim(fgets(STDIN)))) {
Expand Down
129 changes: 92 additions & 37 deletions src/Connector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Exonet\Api;

use Exonet\Api\Exceptions\ExonetApiException;
use Exonet\Api\Exceptions\ResponseExceptionHandler;
use Exonet\Api\Structures\ApiResource;
use Exonet\Api\Structures\ApiResourceIdentifier;
Expand All @@ -20,14 +21,19 @@
class Connector
{
/**
* @var GuzzleClient The Guzzle client.
* @var GuzzleClient The HTTP client instance.
*/
private $httpClient;
private static $httpClient;

/**
* @var HandlerStack|null The Guzzle handler stack to use, if not default.
*/
private static $guzzleHandlerStack;

/**
* @var Client The API client.
*/
private $apiClient;
private $apiClientInstance;

/**
* Connector constructor.
Expand All @@ -37,28 +43,25 @@ class Connector
*/
public function __construct(?HandlerStack $guzzleHandlerStack = null, ?Client $client = null)
{
// Don't let Guzzle throw exceptions, as it is handled by this class.
$this->httpClient = new GuzzleClient(['exceptions' => false, 'handler' => $guzzleHandlerStack]);
$this->apiClient = $client ?? Client::getInstance();
self::$guzzleHandlerStack = $guzzleHandlerStack;
$this->apiClientInstance = $client;
}

/**
* Perform a GET request and return the parsed body as response.
*
* @param string $urlPath The URL path to GET.
*
* @throws \Exonet\Api\Exceptions\ExonetApiException If there was a problem with the request.
*
* @return ApiResource|ApiResourceSet The requested URL path transformed to a single or multiple resources.
*/
public function get(string $urlPath)
{
$apiUrl = $this->apiClient->getApiUrl().$urlPath;
$this->apiClient->log()->debug('Sending [GET] request', ['url' => $apiUrl]);
$apiUrl = $this->apiClient()->getApiUrl().$urlPath;
$this->apiClient()->log()->debug('Sending [GET] request', ['url' => $apiUrl]);

$request = new Request('GET', $apiUrl, $this->getDefaultHeaders());

$response = $this->httpClient->send($request);
$response = self::httpClient()->send($request);

return $this->parseResponse($response);
}
Expand All @@ -69,14 +72,12 @@ public function get(string $urlPath)
* @param string $urlPath The URL to post to.
* @param array $data An array with data to post to the API.
*
* @throws \Exonet\Api\Exceptions\ExonetApiException If there was a problem with the request.
*
* @return ApiResource|ApiResourceIdentifier|ApiResourceSet The response from the API, converted to resources.
*/
public function post(string $urlPath, array $data)
{
$apiUrl = $this->apiClient->getApiUrl().$urlPath;
$this->apiClient->log()->debug('Sending [POST] request', ['url' => $apiUrl]);
$apiUrl = $this->apiClient()->getApiUrl().$urlPath;
$this->apiClient()->log()->debug('Sending [POST] request', ['url' => $apiUrl]);

$request = new Request(
'POST',
Expand All @@ -85,28 +86,55 @@ public function post(string $urlPath, array $data)
json_encode($data)
);

$response = $this->httpClient->send($request);
$response = self::httpClient()->send($request);

return $this->parseResponse($response);
}

/**
* Convert the data to JSON and patch it to a URL.
*
* @param string $urlPath The URL to patch to.
* @param array $data An array with data to patch to the API.
*
* @return bool True when the patch succeeded.
*/
public function patch(string $urlPath, array $data) : bool
{
$apiUrl = $this->apiClient()->getApiUrl().$urlPath;
$this->apiClient()->log()->debug('Sending [PATCH] request', ['url' => $apiUrl]);

$request = new Request(
'PATCH',
$apiUrl,
$this->getDefaultHeaders(),
json_encode($data)
);

self::httpClient()->send($request);

return true;
}

/**
* Make a DELETE call to the API.
*
* @param string $urlPath The url to make the DELETE request to.
* @param array $data (Optional) The data to send along with the DELETE request.
*/
public function delete(string $urlPath)
public function delete(string $urlPath, array $data = []) : void
{
$apiUrl = $this->apiClient->getApiUrl().$urlPath;
$this->apiClient->log()->debug('Sending [DELETE] request', ['url' => $apiUrl]);
$apiUrl = $this->apiClient()->getApiUrl().$urlPath;
$this->apiClient()->log()->debug('Sending [DELETE] request', ['url' => $apiUrl]);

$request = new Request(
'DELETE',
$apiUrl,
$this->getDefaultHeaders()
$this->getDefaultHeaders(),
json_encode($data)
);

$this->httpClient->send($request);
self::httpClient()->send($request);
}

/**
Expand All @@ -115,33 +143,60 @@ public function delete(string $urlPath)
*
* @param PsrResponse $response The call response.
*
* @throws \Exonet\Api\Exceptions\ExonetApiException If there was a problem with the request.
* @throws ExonetApiException If there was a problem with the request.
*
* @return ApiResourceIdentifier|ApiResource|ApiResourceSet The structured response.
*/
private function parseResponse(PsrResponse $response)
{
$this->apiClient->log()->debug('Request completed', ['statusCode' => $response->getStatusCode()]);
$this->apiClient()->log()->debug('Request completed', ['statusCode' => $response->getStatusCode()]);

if ($response->getStatusCode() < 300) {
$contents = $response->getBody()->getContents();
if ($response->getStatusCode() >= 300) {
(new ResponseExceptionHandler($response))->handle();
}

$decodedContent = json_decode($contents);
$contents = $response->getBody()->getContents();

// Create collection of resources when returned data is an array.
if (is_array($decodedContent->data)) {
return new ApiResourceSet($contents);
}
$decodedContent = json_decode($contents);

// Convert single item into resource or resource identifier.
if (isset($decodedContent->data->attributes)) {
return new ApiResource($decodedContent->data->type, $contents);
}
// Create collection of resources when returned data is an array.
if (is_array($decodedContent->data)) {
return new ApiResourceSet($contents);
}

return new ApiResourceIdentifier($decodedContent->data->type, $decodedContent->data->id);
// Convert single item into resource or resource identifier.
if (isset($decodedContent->data->attributes)) {
return new ApiResource($decodedContent->data->type, $contents);
}

(new ResponseExceptionHandler($response))->handle();
return new ApiResourceIdentifier($decodedContent->data->type, $decodedContent->data->id);
}

/**
* Get or create an HTTP client based on the configured handler stack. Implement the singleton pattern so the HTTP
* client is shared.
*
* @return GuzzleClient The HTTP client instance.
*/
private static function httpClient() : GuzzleClient
{
$stackHash = spl_object_hash(self::$guzzleHandlerStack ?? new \stdClass());
if (!isset(self::$httpClient[$stackHash])) {
// Don't let Guzzle throw exceptions, as it is handled by this class.
self::$httpClient[$stackHash] = new GuzzleClient(['exceptions' => false, 'handler' => self::$guzzleHandlerStack]);
}

return self::$httpClient[$stackHash];
}

/**
* Get the API client.
*
* @return Client The API client.
*/
private function apiClient() : Client
{
return $this->apiClientInstance ?? Client::getInstance();
}

/**
Expand All @@ -152,7 +207,7 @@ private function parseResponse(PsrResponse $response)
private function getDefaultHeaders() : array
{
return [
'Authorization' => sprintf('Bearer %s', $this->apiClient->getAuth()->getToken()),
'Authorization' => sprintf('Bearer %s', $this->apiClient()->getAuth()->getToken()),
'Accept' => 'application/vnd.Exonet.v1+json',
'Content-Type' => 'application/json',
'User-Agent' => 'exonet-api-php/'.Client::CLIENT_VERSION,
Expand Down
Loading

0 comments on commit 382f43c

Please sign in to comment.