Skip to content

Instantly share code, notes, and snippets.

@jvelo
Created May 15, 2013 21:39
Show Gist options
  • Save jvelo/5587625 to your computer and use it in GitHub Desktop.
Save jvelo/5587625 to your computer and use it in GitHub Desktop.
Mayocat marketplace API sample client in PHP
<?php
require_once 'PestJSON.php';
$api_endpoint = "http://localhost:8080/marketplace/api/";
$client = new PestJSON($api_endpoint);
$number_of_products_per_page = 5;
$result = $client->get('products?number=' . $number_of_products_per_page . '&offset=0');
// Uncomment to see the products result JSON
// echo '<pre>';var_dump($result);echo '</pre>';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Marketplace API client example</title>
<link rel="stylesheet" href="css/style.css">
<!--[if IE]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body id="home">
<ul class="products">
<?php foreach($result['items'] as $product) { ?>
<li class="product">
<h2> <!-- product's title -->
<?php echo $product['properties']['title']; ?>
</h2>
<div class="image">
<?php
if ($product['featuredImage']) {
echo "<img src='" . $product['featuredImage']['url'] . "?width=100&height=100' title='" . $product['featuredImage']['title'] . "' />";
}
else {
// No featured image... insert sample image.
}
?>
</div>
<div class="merchant">
Sold by <i><?php echo $product['site']['properties']['name']; ?></i>
</div>
</li>
<?php } ?>
</ul>
</body>
</html>
<?php // -*- c-basic-offset: 2 -*-
/**
* Pest is a REST client for PHP.
*
* See http://github.com/educoder/pest for details.
*
* This code is licensed for use, modification, and distribution
* under the terms of the MIT License (see http://en.wikipedia.org/wiki/MIT_License)
*/
class Pest {
public $curl_opts = array(
CURLOPT_RETURNTRANSFER => true, // return result instead of echoing
CURLOPT_SSL_VERIFYPEER => false, // stop cURL from verifying the peer's certificate
CURLOPT_FOLLOWLOCATION => false, // follow redirects, Location: headers
CURLOPT_MAXREDIRS => 10 // but dont redirect more than 10 times
);
public $base_url;
public $last_response;
public $last_request;
public $last_headers;
public $throw_exceptions = true;
public function __construct($base_url) {
if (!function_exists('curl_init')) {
throw new Exception('CURL module not available! Pest requires CURL. See http://php.net/manual/en/book.curl.php');
}
// only enable CURLOPT_FOLLOWLOCATION if safe_mode and open_base_dir are not in use
if(ini_get('open_basedir') == '' && strtolower(ini_get('safe_mode')) == 'off') {
$this->curl_opts['CURLOPT_FOLLOWLOCATION'] = true;
}
$this->base_url = $base_url;
// The callback to handle return headers
// Using PHP 5.2, it cannot be initialised in the static context
$this->curl_opts[CURLOPT_HEADERFUNCTION] = array($this, 'handle_header');
}
// $auth can be 'basic' or 'digest'
public function setupAuth($user, $pass, $auth = 'basic') {
$this->curl_opts[CURLOPT_HTTPAUTH] = constant('CURLAUTH_'.strtoupper($auth));
$this->curl_opts[CURLOPT_USERPWD] = $user . ":" . $pass;
}
// Enable a proxy
public function setupProxy($host, $port, $user = NULL, $pass = NULL) {
$this->curl_opts[CURLOPT_PROXYTYPE] = 'HTTP';
$this->curl_opts[CURLOPT_PROXY] = $host;
$this->curl_opts[CURLOPT_PROXYPORT] = $port;
if ($user && $pass) {
$this->curl_opts[CURLOPT_PROXYUSERPWD] = $user . ":" . $pass;
}
}
public function get($url, $data=array()) {
if (!empty($data)) {
$pos = strpos($url, '?');
if ($pos !== false) {
$url = substr($url, 0, $pos);
}
$url .= '?' . http_build_query($data);
}
$curl = $this->prepRequest($this->curl_opts, $url);
$body = $this->doRequest($curl);
$body = $this->processBody($body);
return $body;
}
public function head($url) {
$curl_opts = $this->curl_opts;
$curl_opts[CURLOPT_NOBODY] = true;
$curl = $this->prepRequest($this->curl_opts, $url);
$body = $this->doRequest($curl);
$body = $this->processBody($body);
return $body;
}
public function prepData($data) {
if (is_array($data)) {
$multipart = false;
foreach ($data as $item) {
if (is_string($item) && strncmp($item, "@", 1) == 0 && is_file(substr($item, 1))) {
$multipart = true;
break;
}
}
return ($multipart) ? $data : http_build_query($data);
} else {
return $data;
}
}
public function post($url, $data, $headers=array()) {
$data = $this->prepData($data);
$curl_opts = $this->curl_opts;
$curl_opts[CURLOPT_CUSTOMREQUEST] = 'POST';
if (!is_array($data)) $headers[] = 'Content-Length: '.strlen($data);
$curl_opts[CURLOPT_HTTPHEADER] = array_merge($headers,$curl_opts[CURLOPT_HTTPHEADER]);
$curl_opts[CURLOPT_POSTFIELDS] = $data;
$curl = $this->prepRequest($curl_opts, $url);
$body = $this->doRequest($curl);
$body = $this->processBody($body);
return $body;
}
public function put($url, $data, $headers=array()) {
$data = $this->prepData($data);
$curl_opts = $this->curl_opts;
$curl_opts[CURLOPT_CUSTOMREQUEST] = 'PUT';
if (!is_array($data)) $headers[] = 'Content-Length: '.strlen($data);
$curl_opts[CURLOPT_HTTPHEADER] = $headers;
$curl_opts[CURLOPT_POSTFIELDS] = $data;
$curl = $this->prepRequest($curl_opts, $url);
$body = $this->doRequest($curl);
$body = $this->processBody($body);
return $body;
}
public function patch($url, $data, $headers=array()) {
$data = (is_array($data)) ? http_build_query($data) : $data;
$curl_opts = $this->curl_opts;
$curl_opts[CURLOPT_CUSTOMREQUEST] = 'PATCH';
$headers[] = 'Content-Length: '.strlen($data);
$curl_opts[CURLOPT_HTTPHEADER] = $headers;
$curl_opts[CURLOPT_POSTFIELDS] = $data;
$curl = $this->prepRequest($curl_opts, $url);
$body = $this->doRequest($curl);
$body = $this->processBody($body);
return $body;
}
public function delete($url) {
$curl_opts = $this->curl_opts;
$curl_opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
$curl = $this->prepRequest($curl_opts, $url);
$body = $this->doRequest($curl);
$body = $this->processBody($body);
return $body;
}
public function lastBody() {
return $this->last_response['body'];
}
public function lastStatus() {
return $this->last_response['meta']['http_code'];
}
/**
* Return the last response header (case insensitive) or NULL if not present.
* HTTP allows empty headers (e.g. RFC 2616, Section 14.23), thus is_null()
* and not negation or empty() should be used.
*/
public function lastHeader($header) {
if (empty($this->last_headers[strtolower($header)])) {
return NULL;
}
return $this->last_headers[strtolower($header)];
}
protected function processBody($body) {
// Override this in classes that extend Pest.
// The body of every GET/POST/PUT/DELETE response goes through
// here prior to being returned.
return $body;
}
protected function processError($body) {
// Override this in classes that extend Pest.
// The body of every erroneous (non-2xx/3xx) GET/POST/PUT/DELETE
// response goes through here prior to being used as the 'message'
// of the resulting Pest_Exception
return $body;
}
protected function prepRequest($opts, $url) {
if (strncmp($url, $this->base_url, strlen($this->base_url)) != 0) {
$url = rtrim($this->base_url, '/') . '/' . ltrim($url, '/');
}
$curl = curl_init($url);
foreach ($opts as $opt => $val)
curl_setopt($curl, $opt, $val);
$this->last_request = array(
'url' => $url
);
if (isset($opts[CURLOPT_CUSTOMREQUEST]))
$this->last_request['method'] = $opts[CURLOPT_CUSTOMREQUEST];
else
$this->last_request['method'] = 'GET';
if (isset($opts[CURLOPT_POSTFIELDS]))
$this->last_request['data'] = $opts[CURLOPT_POSTFIELDS];
return $curl;
}
private function handle_header($ch, $str) {
if (preg_match('/([^:]+):\s(.+)/m', $str, $match) ) {
$this->last_headers[strtolower($match[1])] = trim($match[2]);
}
return strlen($str);
}
private function doRequest($curl) {
$this->last_headers = array();
$body = curl_exec($curl);
$meta = curl_getinfo($curl);
$this->last_response = array(
'body' => $body,
'meta' => $meta
);
curl_close($curl);
$this->checkLastResponseForError();
return $body;
}
protected function checkLastResponseForError() {
if ( !$this->throw_exceptions)
return;
$meta = $this->last_response['meta'];
$body = $this->last_response['body'];
if (!$meta)
return;
$err = null;
switch ($meta['http_code']) {
case 400:
throw new Pest_BadRequest($this->processError($body));
break;
case 401:
throw new Pest_Unauthorized($this->processError($body));
break;
case 403:
throw new Pest_Forbidden($this->processError($body));
break;
case 404:
throw new Pest_NotFound($this->processError($body));
break;
case 405:
throw new Pest_MethodNotAllowed($this->processError($body));
break;
case 409:
throw new Pest_Conflict($this->processError($body));
break;
case 410:
throw new Pest_Gone($this->processError($body));
break;
case 422:
// Unprocessable Entity -- see http://www.iana.org/assignments/http-status-codes
// This is now commonly used (in Rails, at least) to indicate
// a response to a request that is syntactically correct,
// but semantically invalid (for example, when trying to
// create a resource with some required fields missing)
throw new Pest_InvalidRecord($this->processError($body));
break;
default:
if ($meta['http_code'] >= 400 && $meta['http_code'] <= 499)
throw new Pest_ClientError($this->processError($body));
elseif ($meta['http_code'] >= 500 && $meta['http_code'] <= 599)
throw new Pest_ServerError($this->processError($body));
elseif (!$meta['http_code'] || $meta['http_code'] >= 600) {
throw new Pest_UnknownResponse($this->processError($body));
}
}
}
}
class Pest_Exception extends Exception { }
class Pest_UnknownResponse extends Pest_Exception { }
/* 401-499 */ class Pest_ClientError extends Pest_Exception {}
/* 400 */ class Pest_BadRequest extends Pest_ClientError {}
/* 401 */ class Pest_Unauthorized extends Pest_ClientError {}
/* 403 */ class Pest_Forbidden extends Pest_ClientError {}
/* 404 */ class Pest_NotFound extends Pest_ClientError {}
/* 405 */ class Pest_MethodNotAllowed extends Pest_ClientError {}
/* 409 */ class Pest_Conflict extends Pest_ClientError {}
/* 410 */ class Pest_Gone extends Pest_ClientError {}
/* 422 */ class Pest_InvalidRecord extends Pest_ClientError {}
/* 500-599 */ class Pest_ServerError extends Pest_Exception {}
?>
<?php
require_once 'Pest.php';
/**
* Small Pest addition by Egbert Teeselink (http://www.github.com/eteeselink)
*
* Pest is a REST client for PHP.
* PestJSON adds JSON-specific functionality to Pest, automatically converting
* JSON data resturned from REST services into PHP arrays and vice versa.
*
* In other words, while Pest's get/post/put/delete calls return raw strings,
* PestJSON return (associative) arrays.
*
* In case of >= 400 status codes, an exception is thrown with $e->getMessage()
* containing the error message that the server produced. User code will have to
* json_decode() that manually, if applicable, because the PHP Exception base
* class does not accept arrays for the exception message and some JSON/REST servers
* do not produce nice JSON
*
* See http://github.com/educoder/pest for details.
*
* This code is licensed for use, modification, and distribution
* under the terms of the MIT License (see http://en.wikipedia.org/wiki/MIT_License)
*/
class PestJSON extends Pest
{
public function post($url, $data, $headers=array()) {
return parent::post($url, json_encode($data), $headers);
}
public function put($url, $data, $headers=array()) {
return parent::put($url, json_encode($data), $headers);
}
protected function prepRequest($opts, $url) {
$opts[CURLOPT_HTTPHEADER][] = 'Accept: application/json';
$opts[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
return parent::prepRequest($opts, $url);
}
public function processBody($body) {
return json_decode($body, true);
}
}
{
"first": {
"href": "http://localhost:8080/marketplace/api/products?number=3&offset=0?number=3&offset=0"
},
"href": "http://localhost:8080/marketplace/api/products?number=3&offset=0",
"items": [
{
"addons": {},
"featuredImage": {
"extension": "jpg",
"title": "undefined",
"url": "http://shop.localhost:8080/images/xwwra.jpg"
},
"properties": {
"description": "",
"onShelf": true,
"price": 7000.0,
"slug": "peugeot-403-convertible",
"stock": null,
"title": "Peugeot 403 Convertible"
},
"site": {
"properties": {
"defaultHost": null,
"name": "my shop"
},
"url": "http://shop.localhost:8080"
},
"url": "http://shop.localhost:8080/products/peugeot-403-convertible"
},
{
"addons": {},
"featuredImage": {
"extension": "jpg",
"title": "undefined",
"url": "http://shop.localhost:8080/images/29ecgkDg.jpg"
},
"properties": {
"description": "",
"onShelf": true,
"price": 200.0,
"slug": "cortefiel-raincoat",
"stock": null,
"title": "Cortefiel Raincoat"
},
"site": {
"properties": {
"defaultHost": null,
"name": "my shop"
},
"url": "http://shop.localhost:8080"
},
"url": "http://shop.localhost:8080/products/cortefiel-raincoat"
},
{
"addons": {
"theme": {
"characteristics": {
"brand": "my brand",
"manufacturer": "my manufacturer"
}
}
},
"featuredImage": {
"extension": "jpg",
"title": "undefined",
"url": "http://shop.localhost:8080/images/dpg2m.jpg"
},
"properties": {
"description": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam quis tortor a orci viverra commodo ut quis augue. Donec massa tellus, consequat quis volutpat nec, sodales ut ipsum. Integer aliquam eleifend urna, a mattis nulla auctor quis. Fusce facilisis volutpat velit, quis mattis enim rutrum ac. Praesent blandit, dolor sit amet pretium varius, nisi sapien porta augue, a vulputate velit tellus nec metus. Aliquam mollis dolor sed orci aliquet gravida. Donec tempus, ante eu dapibus pharetra, sem leo tempus sapien, at volutpat urna mauris ac orci. Fusce at libero a nisi bibendum pretium vel et nisi. Cras mauris nisi, luctus placerat euismod id, laoreet at ipsum. Suspendisse potenti. Ut accumsan sollicitudin ante a pellentesque. Cras convallis bibendum ornare.</p>\n",
"onShelf": true,
"price": 700.0,
"slug": "basset-hound",
"stock": null,
"title": "Basset Hound"
},
"site": {
"properties": {
"defaultHost": null,
"name": "my shop"
},
"url": "http://shop.localhost:8080"
},
"url": "http://shop.localhost:8080/products/basset-hound"
}
],
"last": {
"href": "http://localhost:8080/marketplace/api/products?number=3&offset=0?number=3&offset=0"
},
"next": {
"href": "http://localhost:8080/marketplace/api/products?number=3&offset=0?number=3&offset=3"
},
"number": 3,
"offset": 0,
"previous": null,
"total": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment