updated balanced library

This commit is contained in:
arzynik 2013-05-14 12:06:44 -07:00
parent 903f1eed06
commit 8bab676488
42 changed files with 1679 additions and 1111 deletions

View File

@ -27,7 +27,7 @@ $GLOBALS['config'] = [
'root' => dirname(__FILE__).'/../', 'root' => dirname(__FILE__).'/../',
'www' => dirname(__FILE__).'/../www/', 'www' => dirname(__FILE__).'/../www/',
'storage' => dirname(__FILE__).'/../storage/', 'storage' => dirname(__FILE__).'/../storage/',
],'libraries' => ['Crunchbutton','Cana','Services','Balanced','Ordrin','QueryPath','Github'], ],'libraries' => ['Crunchbutton','Cana','Services','Balanced','RESTful','Ordrin','QueryPath','Github'],
'alias' => [] 'alias' => []
]; ];
@ -91,6 +91,7 @@ spl_autoload_register(function ($className) {
\Httpful\Bootstrap::init(); \Httpful\Bootstrap::init();
\RESTful\Bootstrap::init();
\Balanced\Bootstrap::init(); \Balanced\Bootstrap::init();
\Ordrin\Bootstrap::init(); \Ordrin\Bootstrap::init();
\QueryPath\Bootstrap::init(); \QueryPath\Bootstrap::init();

View File

@ -2,12 +2,12 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec;
use Balanced\Settings; use Balanced\Settings;
use \RESTful\URISpec;
/** /**
* Represents an api key. These are used to autheticate you with the api. * Represents an api key. These are used to authenticate you with the api.
* *
* Typically you create an initial api key: * Typically you create an initial api key:
* *
@ -41,15 +41,15 @@ use Balanced\Settings;
* ->sort(\Balanced\APIKey::f->created_at->desc()) * ->sort(\Balanced\APIKey::f->created_at->desc())
* ->first() * ->first()
* ->delete(); * ->delete();
* </code> * </code>
*/ */
class APIKey extends Resource class APIKey extends Resource
{ {
protected static $_uri_spec = null; protected static $_uri_spec = null;
public static function init() public static function init()
{ {
self::$_uri_spec = new URISpec('api_keys', 'id', '/v1'); self::$_uri_spec = new URISpec('api_keys', 'id', '/v1');
self::$_registry->add(get_called_class()); self::$_registry->add(get_called_class());
} }
} }

View File

@ -2,8 +2,8 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use \RESTful\URISpec;
/** /**
* Represent a buyer or merchant account on a marketplace. * Represent a buyer or merchant account on a marketplace.
@ -94,25 +94,43 @@ class Account extends Resource
$appears_on_statement_as = null, $appears_on_statement_as = null,
$description = null, $description = null,
$meta = null, $meta = null,
$source = null) $source = null,
$on_behalf_of = null)
{ {
if ($source == null) if ($source == null) {
$source_uri = null; $source_uri = null;
else } else if (is_string($source)) {
$source_uri = is_string($source) ? $source : $source->uri; $source_uri = $source;
} else {
$source_uri = $source->uri;
}
if ($on_behalf_of == null) {
$on_behalf_of_uri = null;
} else if (is_string($on_behalf_of)) {
$on_behalf_of_uri = $on_behalf_of;
} else {
$on_behalf_of_uri = $on_behalf_of->uri;
}
if (isset($this->uri) && $on_behalf_of_uri == $this->uri)
throw new \InvalidArgumentException(
'The on_behalf_of parameter MAY NOT be the same account as the account you are debiting!'
);
return $this->debits->create(array( return $this->debits->create(array(
'amount' => $amount, 'amount' => $amount,
'appears_on_statement_as' => $appears_on_statement_as,
'description' => $description, 'description' => $description,
'meta' => $meta, 'meta' => $meta,
'source_uri' => $source_uri, 'source_uri' => $source_uri,
'on_behalf_of_uri' => $on_behalf_of_uri,
'appears_on_statement_as' => $appears_on_statement_as 'appears_on_statement_as' => $appears_on_statement_as
)); ));
} }
/** /**
* Create a hold (i.e. a guaranteed pending debit) for account funds. You * Create a hold (i.e. a guaranteed pending debit) for account funds. You
* can later capture or void. A hold is assocaited with a account funding * can later capture or void. A hold is associated with a account funding
* source (i.e. \Balanced\Card). If you don't specify the source then the * source (i.e. \Balanced\Card). If you don't specify the source then the
* current primary funding source for the account is used. * current primary funding source for the account is used.
* *
@ -145,7 +163,7 @@ class Account extends Resource
* *
* @param mixed card \Balanced\Card or URI referencing a card to associate with the account. Alternatively it can be an associative array describing a card to create and associate with the account. * @param mixed card \Balanced\Card or URI referencing a card to associate with the account. Alternatively it can be an associative array describing a card to create and associate with the account.
* *
* @return Balanced\Account * @return \Balanced\Account
*/ */
public function addCard($card) public function addCard($card)
{ {
@ -164,9 +182,9 @@ class Account extends Resource
* *
* @see \Balanced\Marketplace->createBankAccount * @see \Balanced\Marketplace->createBankAccount
* *
* @param mixed bank_account \Balanced\BankAccount or URI for a bank account to assocaite with the account. Alternatively it can be an associative array describing a bank account to create and associate with the account. * @param mixed bank_account \Balanced\BankAccount or URI for a bank account to associate with the account. Alternatively it can be an associative array describing a bank account to create and associate with the account.
*
* @return Balanced\Account * @return \Balanced\Account
*/ */
public function addBankAccount($bank_account) public function addBankAccount($bank_account)
{ {
@ -186,11 +204,14 @@ class Account extends Resource
* *
* @param mixed merchant Associative array describing the merchants identity or a URI referencing a created merchant. * @param mixed merchant Associative array describing the merchants identity or a URI referencing a created merchant.
* *
* @return Balanced\Account * @return \Balanced\Account
*/ */
public function promoteToMerchant($merchant) public function promoteToMerchant($merchant)
{ {
$this->merchant = $merchant; if (is_string($merchant))
$this->merchant_uri = $merchant;
else
$this->merchant = $merchant;
return $this->save(); return $this->save();
} }
} }

View File

@ -2,8 +2,8 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use \RESTful\URISpec;
/** /**
* Represents an account bank account. * Represents an account bank account.
@ -41,20 +41,87 @@ class BankAccount extends Resource
self::$_registry->add(get_called_class()); self::$_registry->add(get_called_class());
} }
public function credit( /**
$amount, * Credit a bank account.
$description = null, *
$meta = null, * @param int amount Amount to credit in USD pennies.
$appears_on_statement_as = null) * @param string description Optional description of the credit.
* @param string appears_on_statement_as Optional description of the credit as it will appears on the customer's billing statement.
*
* @return \Balanced\Credit
*
* <code>
* $bank_account = new \Balanced\BankAccount(array(
* 'account_number' => '12341234',
* 'name' => 'Fit Finlay',
* 'bank_code' => '325182797',
* 'type' => 'checking',
* ));
*
* $credit = $bank_account->credit(123, 'something descriptive');
* </code>
*/
public function credit(
$amount,
$description = null,
$meta = null,
$appears_on_statement_as = null)
{ {
if ($this->account == null) { if (!property_exists($this, 'account') || $this->account == null) {
throw new \UnexpectedValueException('Bank account is not associated with an account.'); $credit = $this->credits->create(array(
} 'amount' => $amount,
return $this->account->credit( 'description' => $description,
$amount, ));
$description, } else {
$meta, $credit = $this->account->credit(
$this->uri, $amount,
$appears_on_statement_as); $description,
$meta,
$this->uri,
$appears_on_statement_as
);
}
return $credit;
}
public function verify()
{
$response = self::getClient()->post(
$this->verifications_uri, null
);
$verification = new BankAccountVerification();
$verification->_objectify($response->body);
return $verification;
}
}
/**
* Represents an verification for a bank account which is a pre-requisite if
* you want to create debits using the associated bank account. The side-effect
* of creating a verification is that 2 random amounts will be deposited into
* the account which must then be confirmed via the confirm method to ensure
* that you have access to the bank account in question.
*
* You can create these via Balanced\Marketplace::bank_accounts::verify.
*
* <code>
* $marketplace = \Balanced\Marketplace::mine();
*
* $bank_account = $marketplace->bank_accounts->create(array(
* 'name' => 'name',
* 'account_number' => '11223344',
* 'bank_code' => '1313123',
* ));
*
* $verification = $bank_account->verify();
* </code>
*/
class BankAccountVerification extends Resource {
public function confirm($amount1, $amount2) {
$this->amount_1 = $amount1;
$this->amount_2 = $amount2;
$this->save();
return $this;
} }
} }

View File

@ -1,72 +1,77 @@
<?php <?php
namespace Balanced; namespace Balanced;
require_once(dirname(dirname(__FILE__)).'/Balanced/Errors.php');
/** /**
* Bootstrapper for Balanced does autoloading and resource initialization. * Bootstrapper for Balanced does autoloading and resource initialization.
*/ */
class Bootstrap class Bootstrap
{ {
const DIR_SEPARATOR = DIRECTORY_SEPARATOR; const DIR_SEPARATOR = DIRECTORY_SEPARATOR;
const NAMESPACE_SEPARATOR = '\\'; const NAMESPACE_SEPARATOR = '\\';
public static $initialized = false; public static $initialized = false;
public static function init() public static function init()
{ {
spl_autoload_register(array('\Balanced\Bootstrap', 'autoload')); spl_autoload_register(array('\Balanced\Bootstrap', 'autoload'));
self::initializeResources(); self::initializeResources();
} }
public static function autoload($classname)
{
self::_autoload(dirname(dirname(__FILE__)), $classname);
}
public static function pharInit()
{
spl_autoload_register(array('\Balanced\Bootstrap', 'pharAutoload'));
self::initializeResources();
}
public static function pharAutoload($classname)
{
self::_autoload('phar://balanced.phar', $classname);
}
private static function _autoload($base, $classname)
{
$parts = explode(self::NAMESPACE_SEPARATOR, $classname);
$path = $base . self::DIR_SEPARATOR. implode(self::DIR_SEPARATOR, $parts) . '.php';
if (file_exists($path)) {
require_once($path);
}
}
/** public static function autoload($classname)
* Initializes resources (i.e. registers them with Resource::_registry). Note {
* that if you add a Resource then you must initialize it here. self::_autoload(dirname(dirname(__FILE__)), $classname);
* }
* @internal
*/ public static function pharInit()
private static function initializeResources() {
{ spl_autoload_register(array('\Balanced\Bootstrap', 'pharAutoload'));
if (self::$initialized) self::initializeResources();
return; }
\Balanced\Core\Resource::init(); public static function pharAutoload($classname)
\Balanced\APIKey::init(); {
\Balanced\Marketplace::init(); self::_autoload('phar://balanced.phar', $classname);
\Balanced\Account::init(); }
\Balanced\Credit::init();
\Balanced\Debit::init(); private static function _autoload($base, $classname) {
\Balanced\Refund::init(); if (!strncmp($classname, 'Balanced\Errors\\', strlen('Balanced\Errors\\'))) {
\Balanced\Card::init(); $classname = 'Balanced\Errors';
\Balanced\BankAccount::init(); }
\Balanced\Hold::init(); $parts = explode(self::NAMESPACE_SEPARATOR, $classname);
\Balanced\Merchant::init(); $path = $base . self::DIR_SEPARATOR. implode(self::DIR_SEPARATOR, $parts) . '.php';
if (file_exists($path)) {
self::$initialized = true; require_once($path);
} }
}
/**
* Initializes resources (i.e. registers them with Resource::_registry). Note
* that if you add a Resource then you must initialize it here.
*
* @internal
*/
private static function initializeResources() {
if (self::$initialized)
return;
\Balanced\Errors\Error::init();
\Balanced\Resource::init();
\Balanced\APIKey::init();
\Balanced\Marketplace::init();
\Balanced\Account::init();
\Balanced\Credit::init();
\Balanced\Debit::init();
\Balanced\Refund::init();
\Balanced\Card::init();
\Balanced\BankAccount::init();
\Balanced\Hold::init();
\Balanced\Merchant::init();
\Balanced\Callback::init();
\Balanced\Event::init();
self::$initialized = true;
}
} }

View File

@ -0,0 +1,24 @@
<?php
namespace Balanced;
use Balanced\Resource;
use \RESTful\URISpec;
/*
* A Callback is a publicly accessible location that can receive POSTed JSON
* data whenever an Event is generated.
*
* You create these using Balanced\Marketplace->createCallback.
*
*/
class Callback extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('callbacks', 'id');
self::$_registry->add(get_called_class());
}
}

View File

@ -2,8 +2,8 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use \RESTful\URISpec;
/** /**
* Represents an account card. * Represents an account card.
@ -30,32 +30,32 @@ use Balanced\Core\URISpec;
* ->one(); * ->one();
* $account->addCard($card->uri); * $account->addCard($card->uri);
* </code> * </code>
*/ */
class Card extends Resource class Card extends Resource
{ {
protected static $_uri_spec = null; protected static $_uri_spec = null;
public static function init() public static function init()
{ {
self::$_uri_spec = new URISpec('cards', 'id', '/v1'); self::$_uri_spec = new URISpec('cards', 'id', '/v1');
self::$_registry->add(get_called_class()); self::$_registry->add(get_called_class());
} }
public function debit( public function debit(
$amount, $amount,
$appears_on_statement_as = null, $appears_on_statement_as = null,
$description = null, $description = null,
$meta = null, $meta = null,
$source = null) $source = null)
{ {
if ($this->account == null) { if ($this->account == null) {
throw new \UnexpectedValueException('Card is not associated with an account.'); throw new \UnexpectedValueException('Card is not associated with an account.');
} }
return $this->account->debit( return $this->account->debit(
$amount, $amount,
$appears_on_statement_as, $appears_on_statement_as,
$description, $description,
$meta, $meta,
$this->uri); $this->uri);
} }
} }

View File

@ -1,56 +0,0 @@
<?php
namespace Balanced\Core;
use Balanced\Exceptions\HTTPError;
use Balanced\Settings;
use Httpful\Request;
class Client
{
public function __construct($request_class = null)
{
$this->request_class = $request_class == null ? 'Request' : $request_class;
}
public function get($uri)
{
$url = Settings::$url_root . $uri;
$request = \Httpful\Request::get($url);
return $this->_op($request);
}
public function post($uri, $payload)
{
$url = Settings::$url_root . $uri;
$request = Request::post($url, $payload, 'json');
return $this->_op($request);
}
public function put($uri, $payload)
{
$url = Settings::$url_root . $uri;
$request = Request::put($url, $payload, 'json');
return $this->_op($request);
}
public function delete($uri)
{
$url = Settings::$url_root . $uri;
$request = Request::delete($url);
return $this->_op($request);
}
private function _op($request)
{
$user_agent = 'balanced-php/' . Settings::VERSION;
$request->headers['User-Agent'] = $user_agent;
if (Settings::$api_key != null)
$request = $request->authenticateWith(Settings::$api_key , '');
$request->expects('json');
$response = $request->sendIt();
if ($response->hasErrors() || $response->code == 300)
throw new HTTPError($response);
return $response;
}
}

View File

@ -1,299 +0,0 @@
<?php
namespace Balanced\Core;
class Resource
{
public static $fields,
$f;
protected static $_client,
$_registry,
$_uri_spec;
protected $_collection_uris,
$_member_uris;
public static function init()
{
self::$_client = new Client();
self::$_registry = new Registry();
self::$f = self::$fields = new Fields();
}
public static function getClient()
{
$class = get_called_class();
return $class::$_client;
}
public static function getRegistry()
{
$class = get_called_class();
return $class::$_registry;
}
public static function getURISpec()
{
$class = get_called_class();
return $class::$_uri_spec;
}
public function __construct($fields = null)
{
if ($fields == null)
$fields = array();
$this->_objectify($fields);
}
public function __get($name)
{
// collection uri
if (array_key_exists($name, $this->_collection_uris)) {
$result = $this->_collection_uris[$name];
$this->$name = new Collection($result['class'], $result['uri']);
return $this->$name;
}
// member uri
else if (array_key_exists($name, $this->_member_uris)) {
$result = $this->$_collection_uris[$name];
$response = self::getClient().get($result['uri']);
$class = $result['class'];
$this->$name = new $class($response->body);
return $this->$name;
}
// unknown
$trace = debug_backtrace();
trigger_error(
'Undefined property via __get(): ' . $name .
' in ' . $trace[0]['file'] .
' on line ' . $trace[0]['line'],
E_USER_NOTICE);
return null;
}
protected function _objectify($fields)
{
// initialize uris
$this->_collection_uris = array();
$this->_member_uris = array();
foreach ($fields as $key => $val) {
// nested uri
if ((strlen($key) - 3) == strrpos($key, 'uri', 0) && $key != 'uri') {
$result = self::$_registry->match($val);
if ($result != null) {
$name = substr($key, 0, -4);
$class = $result['class'];
if ($result['collection'])
$this->_collection_uris[$name] = array(
'class' => $class,
'uri' => $val,
);
else
$this->_member_uris[$name] = array(
'class' => $class,
'uri' => $val,
);
continue;
}
}
// nested
else if (is_object($val) && property_exists($val, 'uri')) {
$result = self::$_registry->match($val->uri);
if ($result != null) {
$class = $result['class'];
if ($result['collection'])
$this->$key = new Collection($class, $val['uri'], $val);
else
$this->$key = new $class($val);
continue;
}
}
// default
$this->$key = $val;
}
}
public static function query()
{
$uri_spec = self::getURISpec();
if ($uri_spec == null || $uri_spec->collection_uri == null) {
$msg = sprintf('Cannot directly query %s resources', get_called_class());
throw new \LogicException($msg);
}
return new Query(get_called_class(), $uri_spec->collection_uri);
}
public static function get($uri)
{
$response = self::getClient()->get($uri);
$class = get_called_class();
return new $class($response->body);
}
public function save()
{
// payload
$payload = array();
foreach($this as $key => $val) {
if ($key[0] == '_' || is_object($val))
continue;
$payload[$key] = $val;
}
// update
if (array_key_exists('uri', $payload)) {
$uri = $payload['uri'];
unset($payload['uri']);
$response = self::getClient()->put($uri, $payload);
}
// create
else {
$class = get_class($this);
if ($class::$_uri_spec == null || $class::$_uri_spec->collection_uri == null) {
$msg = sprintf('Cannot directly create %s resources', $class);
throw new \LogicException($msg);
}
$response = self::getClient()->post($class::$_uri_spec->collection_uri, $payload);
}
// re-objectify
foreach($this as $key => $val)
unset($this->$key);
$this->_objectify($response->body);
return $this;
}
public function delete()
{
self::getClient()->delete($this->uri);
return $this;
}
}
class Fields
{
public function __get($name)
{
return new Field($name);
}
}
class Field
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
public function __get($name)
{
return new Field($this->name . '.' . $name);
}
public function in($vals)
{
return new FilterExpression($this->name, 'in', $vals, '!in');
}
public function startswith($prefix)
{
if (!is_string($prefix))
throw new \InvalidArgumentException('"startswith" prefix must be a string');
return new FilterExpression($this->name, 'contains', $prefix);
}
public function endswith($suffix)
{
if (!is_string($suffix))
throw new \InvalidArgumentException('"endswith" suffix must be a string');
return new FilterExpression($this->name, 'contains', $suffix);
}
public function contains($fragment)
{
if (!is_string($fragment))
throw new \InvalidArgumentException('"contains" fragment must be a string');
return new FilterExpression($this->name, 'contains', $fragment, '!contains');
}
public function eq($val)
{
return new FilterExpression($this->name, '=', $val, '!eq');
}
public function lt($val)
{
return new FilterExpression($this->name, '<', $val, '>=');
}
public function lte($val)
{
return new FilterExpression($this->name, '<=', $val, '>');
}
public function gt($val)
{
return new FilterExpression($this->name, '>', $val, '<=');
}
public function gte($val)
{
return new FilterExpression($this->name, '>=', $val, '<');
}
public function asc()
{
return new SortExpression($this->name, true);
}
public function desc()
{
return new SortExpression($this->name, false);
}
}
class FilterExpression
{
public $field,
$op,
$val,
$not_op;
public function __construct($field, $op, $val, $not_op = null)
{
$this->field = $field;
$this->op = $op;
$this->val = $val;
$this->not_op = $not_op;
}
public function not()
{
if ($not_op == null)
throw new \LogicException(sprintf('Filter cannot be inverted'));
$temp = $this->op;
$this->op = $this->not_op;
$this->not_op = $temp;
return $this;
}
}
class SortExpression
{
public $name,
$ascending;
public function __construct($field, $ascending = true)
{
$this->field = $field;
$this->ascending= $ascending;
}
}

View File

@ -2,10 +2,10 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use \RESTful\URISpec;
/** /**
* Represents an account credit transaction. * Represents an account credit transaction.
* *
* You create these using Balanced\Account::credit. * You create these using Balanced\Account::credit.
@ -26,16 +26,50 @@ use Balanced\Core\URISpec;
* 'my_id': '112233' * 'my_id': '112233'
* ) * )
* ); * );
* </code> * </code>
*/ */
class Credit extends Resource class Credit extends Resource
{ {
protected static $_uri_spec = null; protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('credits', 'id');
self::$_registry->add(get_called_class());
}
}
public static function init()
{
self::$_uri_spec = new URISpec('credits', 'id', '/v1');
self::$_registry->add(get_called_class());
}
/**
* Credit an unstored bank account.
*
* @param int amount Amount to credit in USD pennies.
* @param string description Optional description of the credit.
* @param mixed bank_account Associative array describing a bank account to credit. The bank account will *not* be stored.
*
* @return \Balanced\Credit
*
* <code>
* $credit = \Balanced\Credit::bankAccount(
* 123,
* array(
* 'account_number' => '12341234',
* 'name' => 'Fit Finlay',
* 'bank_code' => '325182797',
* 'type' => 'checking',
* ),
* 'something descriptive');
* </code>
*/
public static function bankAccount(
$amount,
$bank_account,
$description = null)
{
$credit = new Credit(array(
'amount' => $amount,
'bank_account' => $bank_account,
'description' => $description
));
$credit->save();
return $credit;
}
}

View File

@ -2,23 +2,23 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use \RESTful\URISpec;
/** /**
* Represents an account debit transaction. * Represents an account debit transaction.
* *
* You create these using Balanced\Account::debit. * You create these using Balanced\Account::debit.
* *
* <code> * <code>
* $marketplace = \Balanced\Marketplace::mine(); * $marketplace = \Balanced\Marketplace::mine();
* *
* $account = $marketplace * $account = $marketplace
* ->accounts * ->accounts
* ->query() * ->query()
* ->filter(Account::f->email_address->eq('buyer@example.com')) * ->filter(Account::f->email_address->eq('buyer@example.com'))
* ->one(); * ->one();
* *
* $debit = $account->debit( * $debit = $account->debit(
* 100, * 100,
* 'how it appears on the statement', * 'how it appears on the statement',
@ -27,16 +27,16 @@ use Balanced\Core\URISpec;
* 'my_id': '443322' * 'my_id': '443322'
* ) * )
* ); * );
* </code> * </code>
*/ */
class Debit extends Resource class Debit extends Resource
{ {
protected static $_uri_spec = null; protected static $_uri_spec = null;
public static function init() public static function init()
{ {
self::$_uri_spec = new URISpec('debits', 'id'); self::$_uri_spec = new URISpec('debits', 'id');
self::$_registry->add(get_called_class()); self::$_registry->add(get_called_class());
} }
/** /**
@ -56,9 +56,9 @@ class Debit extends Resource
$meta = null) $meta = null)
{ {
return $this->refunds->create(array( return $this->refunds->create(array(
'amount' => $amount, 'amount' => $amount,
'description' => $description, 'description' => $description,
'meta' => $meta 'meta' => $meta
)); ));
} }
} }

View File

@ -0,0 +1,136 @@
<?php
namespace Balanced\Errors;
use RESTful\Exceptions\HTTPError;
class Error extends HTTPError
{
public static $codes = array();
public static function init()
{
return;
foreach (get_declared_classes() as $class) {
$parent_class = get_parent_class($class);
if ($parent_class != 'Balanced\Errors\Error')
continue;
foreach ($class::$codes as $type)
self::$codes[$type] = $class;
}
}
}
class DuplicateAccountEmailAddress extends Error
{
public static $codes = array('duplicate-email-address');
}
class InvalidAmount extends Error
{
public static $codes = array('invalid-amount');
}
class InvalidRoutingNumber extends Error
{
public static $codes = array('invalid-routing-number');
}
class InvalidBankAccountNumber extends Error
{
public static $codes = array('invalid-bank-account-number');
}
class Declined extends Error
{
public static $codes = array('funding-destination-declined', 'authorization-failed');
}
class CannotAssociateMerchantWithAccount extends Error
{
public static $codes = array('cannot-associate-merchant-with-account');
}
class AccountIsAlreadyAMerchant extends Error
{
public static $codes = array('account-already-merchant');
}
class NoFundingSource extends Error
{
public static $codes = array('no-funding-source');
}
class NoFundingDestination extends Error
{
public static $codes = array('no-funding-destination');
}
class CardAlreadyAssociated extends Error
{
public static $codes = array('card-already-funding-src');
}
class CannotAssociateCard extends Error
{
public static $codes = array('cannot-associate-card');
}
class BankAccountAlreadyAssociated extends Error
{
public static $codes = array('bank-account-already-associated');
}
class AddressVerificationFailed extends Error
{
public static $codes = array('address-verification-failed');
}
class HoldExpired extends Error
{
public static $codes = array('authorization-expired');
}
class MarketplaceAlreadyCreated extends Error
{
public static $codes = array('marketplace-already-created');
}
class IdentityVerificationFailed extends Error
{
public static $codes = array('identity-verification-error', 'business-principal-kyc', 'business-kyc', 'person-kyc');
}
class InsufficientFunds extends Error
{
public static $codes = array('insufficient-funds');
}
class CannotHold extends Error
{
public static $codes = array('funding-source-not-hold');
}
class CannotCredit extends Error
{
public static $codes = array('funding-destination-not-creditable');
}
class CannotDebit extends Error
{
public static $codes = array('funding-source-not-debitable');
}
class CannotRefund extends Error
{
public static $codes = array('funding-source-not-refundable');
}
class BankAccountVerificationFailure extends Error
{
public static $codes = array(
'bank-account-authentication-not-pending',
'bank-account-authentication-failed',
'bank-account-authentication-already-exists'
);
}

View File

@ -0,0 +1,23 @@
<?php
namespace Balanced;
use Balanced\Resource;
use \RESTful\URISpec;
/*
* An Event is a snapshot of another resource at a point in time when
* something significant occurred. Events are created when resources are
* created, updated, deleted or otherwise change state such as a Credit
* being marked as failed.
*/
class Event extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('events', 'id', '/v1');
self::$_registry->add(get_called_class());
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Balanced\Exceptions;
/**
* Base class for all Balanced\Exceptions.
*/
class Base extends \Exception
{
}

View File

@ -1,10 +0,0 @@
<?php
namespace Balanced\Exceptions;
/**
* Indicates that a query unexpectedly returned no results.
*/
class NoResultFound extends Base
{
}

View File

@ -2,15 +2,15 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use \RESTful\URISpec;
/** /**
* Represents pending debit of funds for an account. The funds for that debit * Represents pending debit of funds for an account. The funds for that debit
* are held by the processor. You can later capture the hold, which results in * are held by the processor. You can later capture the hold, which results in
* debit, or void it, which releases the held funds. * debit, or void it, which releases the held funds.
* *
* Note that a hold can expire so you shold always check * Note that a hold can expire so you should always check
* Balanced\Hold::expires_at. * Balanced\Hold::expires_at.
* *
* You create these using \Balanced\Account::hold. * You create these using \Balanced\Account::hold.
@ -38,40 +38,40 @@ use Balanced\Core\URISpec;
*/ */
class Hold extends Resource class Hold extends Resource
{ {
protected static $_uri_spec = null; protected static $_uri_spec = null;
public static function init() public static function init()
{ {
self::$_uri_spec = new URISpec('holds', 'id'); self::$_uri_spec = new URISpec('holds', 'id');
self::$_registry->add(get_called_class()); self::$_registry->add(get_called_class());
} }
/** /**
** Voids a pending hold. This releases the held funds. Once voided a hold ** Voids a pending hold. This releases the held funds. Once voided a hold
* is not longer pending can cannot be re-captured or re-voided. * is not longer pending can cannot be re-captured or re-voided.
* *
* @return Balanced\Hold * @return \Balanced\Hold
*/ */
public function void() public function void()
{ {
$this->is_void = true; $this->is_void = true;
return $this->save(); return $this->save();
} }
/** /**
* Captures a pending hold. This results in a debit. Once captured a hold * Captures a pending hold. This results in a debit. Once captured a hold
* is not longer pending can cannot be re-captured or re-voided. * is not longer pending can cannot be re-captured or re-voided.
* *
* @param int amount Optional Portion of the pending hold to capture. If not specified the full amount associated with the hold is captured. * @param int amount Optional Portion of the pending hold to capture. If not specified the full amount associated with the hold is captured.
* *
* @return Balanced\Debit * @return \Balanced\Debit
*/ */
public function capture($amount = null) public function capture($amount = null)
{ {
$this->debit = $this->account->debits->create(array( $this->debit = $this->account->debits->create(array(
'hold_uri' => $this->uri, 'hold_uri' => $this->uri,
'amount' => $amount, 'amount' => $amount,
)); ));
return $this->debit; return $this->debit;
} }
} }

View File

@ -2,60 +2,62 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use Balanced\Errors;
use Balanced\Account;
use \RESTful\URISpec;
/** /**
* Represents a marketplace. * Represents a marketplace.
* *
* To get started you create an api key and then create a marketplace: * To get started you create an api key and then create a marketplace:
* *
* <code> * <code>
* $api_key = new \Balanced\APIKey(); * $api_key = new \Balanced\APIKey();
* $api_key->save(); * $api_key->save();
* $secret = $api_key->secret // better save this somewhere * $secret = $api_key->secret // better save this somewhere
* print $secret; * print $secret;
* \Balanced\Settings::$api_key = $secret; * \Balanced\Settings::$api_key = $secret;
* *
* $marketplace = new \Balanced\Marketplace(); * $marketplace = new \Balanced\Marketplace();
* $marketplace->save(); * $marketplace->save();
* var_dump($marketplace); * var_dump($marketplace);
* </code> * </code>
* *
* Each api key is uniquely assocaited with an api key so once you've created a * Each api key is uniquely associated with an api key so once you've created a
* marketplace: * marketplace:
* *
* <code> * <code>
* \Balanced\Settings::$api_key = $secret; * \Balanced\Settings::$api_key = $secret;
* $marketplace = \Balanced\Marketplace::mine(); // this is the marketplace associated with $secret * $marketplace = \Balanced\Marketplace::mine(); // this is the marketplace associated with $secret
* </code> * </code>
*/ */
class Marketplace extends Resource class Marketplace extends Resource
{ {
protected static $_uri_spec = null; protected static $_uri_spec = null;
public static function init() public static function init()
{ {
self::$_uri_spec = new URISpec('marketplaces', 'id', '/v1'); self::$_uri_spec = new URISpec('marketplaces', 'id', '/v1');
self::$_registry->add(get_called_class()); self::$_registry->add(get_called_class());
} }
/** /**
* Get the marketplace associated with the currently configured * Get the marketplace associated with the currently configured
* \Balanced\Settings::$api_key. * \Balanced\Settings::$api_key.
* *
* @throws \Balanced\Exceptions\NoResult * @throws \RESTful\Exceptions\NoResultFound
* @return \Balanced\Marketplace * @return \Balanced\Marketplace
*/ */
public static function mine() public static function mine()
{ {
return self::query()->one(); return self::query()->one();
} }
/** /**
* Create a card. These can later be associated with an account using * Create a card. These can later be associated with an account using
* \Balanced\Account->addCard or \Balanced\Marketplace->createBuyer. * \Balanced\Account->addCard or \Balanced\Marketplace->createBuyer.
* *
* @param string street_address Street address. Use null if there is no address for the card. * @param string street_address Street address. Use null if there is no address for the card.
* @param string city City. Use null if there is no address for the card. * @param string city City. Use null if there is no address for the card.
* @param string postal_code Postal code. Use null if there is no address for the card. * @param string postal_code Postal code. Use null if there is no address for the card.
@ -64,7 +66,7 @@ class Marketplace extends Resource
* @param string security_code Card security code. Use null if it is no available. * @param string security_code Card security code. Use null if it is no available.
* @param int expiration_month Expiration month. * @param int expiration_month Expiration month.
* @param int expiration_year Expiration year. * @param int expiration_year Expiration year.
* *
* @return \Balanced\Card * @return \Balanced\Card
*/ */
public function createCard( public function createCard(
@ -94,77 +96,108 @@ class Marketplace extends Resource
'expiration_year' => $expiration_year 'expiration_year' => $expiration_year
)); ));
} }
/** /**
* Create a bank account. These can later be associated with an account * Create a bank account. These can later be associated with an account
* using \Balanced\Account->addBankAccount. * using \Balanced\Account->addBankAccount.
* *
* @param string name Name of the account holder. * @param string name Name of the account holder.
* @param string account_number Account number. * @param string account_number Account number.
* @param string bank_code Bank code or routing number. * @param string routing_number Bank code or routing number.
* * @param string type checking or savings
* @param array meta Single level mapping from string keys to string values.
*
* @return \Balanced\BankAccount * @return \Balanced\BankAccount
*/ */
public function createBankAccount( public function createBankAccount(
$name, $name,
$account_number, $account_number,
$bank_code $routing_number,
$type,
$meta = null
) )
{ {
return $this->bank_accounts->create(array( return $this->bank_accounts->create(array(
'name' => $name, 'name' => $name,
'account_number' => $account_number, 'account_number' => $account_number,
'bank_code' => $bank_code, 'routing_number' => $routing_number,
'type' => $type,
'meta' => $meta
)); ));
} }
/** /**
* Create a role-less account. You can later turn this into a buyer by * Create a role-less account. You can later turn this into a buyer by
* adding a funding source (e.g a card) or a merchant using * adding a funding source (e.g a card) or a merchant using
* \Balanced\Account->promoteToMerchant. * \Balanced\Account->promoteToMerchant.
* *
* @param string email_address Email address. There can only be one account with this email address. * @param string email_address Optional email address. There can only be one account with this email address.
* @param array[string]string meta Optional metadata to associate with the account. * @param array[string]string meta Optional metadata to associate with the account.
* *
* @return \Balanced\Account * @return \Balanced\Account
*/ */
public function createAccount($email_address, $meta = null) public function createAccount($email_address = null, $meta = null)
{ {
return $this->accounts->create(array( return $this->accounts->create(array(
'email_address' => $email_address, 'email_address' => $email_address,
'meta' => $meta, 'meta' => $meta,
)); ));
} }
/** /**
* Create a buyer account. * Find or create a role-less account by email address. You can later turn
* * this into a buyer by adding a funding source (e.g a card) or a merchant
* @param string email_address Email address. There can only be one account with this email address. * using \Balanced\Account->promoteToMerchant.
* @param string card_uri URI referencing a card to associate with the account. *
* @param array[string]string meta Optional metadata to associate with the account. * @param string email_address Email address. There can only be one account with this email address.
* *
* @return \Balanced\Account * @return \Balanced\Account
*/ */
public function createBuyer($email_address, $card_uri, $meta = null) function findOrCreateAccountByEmailAddress($email_address)
{ {
return $this->accounts->create(array( $marketplace = Marketplace::mine();
'email_address' => $email_address, try {
'card_uri' => $card_uri, $account = $this->accounts->create(array(
'meta' => $meta, 'email_address' => $email_address
)); ));
} }
catch (Errors\DuplicateAccountEmailAddress $e) {
$account = Account::get($e->extras->account_uri);
}
return $account;
}
/**
* Create a buyer account.
*
* @param string email_address Optional email address. There can only be one account with this email address.
* @param string card_uri URI referencing a card to associate with the account.
* @param array[string]string meta Optional metadata to associate with the account.
* @param string name Optional name of the account.
*
* @return \Balanced\Account
*/
public function createBuyer($email_address, $card_uri, $meta = null, $name = null)
{
return $this->accounts->create(array(
'email_address' => $email_address,
'card_uri' => $card_uri,
'meta' => $meta,
'name' => $name
));
}
/** /**
* Create a merchant account. * Create a merchant account.
* *
* Unlike buyers the identity of a merchant must be established before * Unlike buyers the identity of a merchant must be established before
* the account can function as a merchant (i.e. be credited). A merchant * the account can function as a merchant (i.e. be credited). A merchant
* can be either a person or a business. Either way that information is * can be either a person or a business. Either way that information is
* represented as an associative array and passed as the merchant parameter * represented as an associative array and passed as the merchant parameter
* when creating the merchant account. * when creating the merchant account.
* *
* For a person the array looks like this: * For a person the array looks like this:
* *
* <code> * <code>
* array( * array(
* 'type' => 'person', * 'type' => 'person',
@ -177,9 +210,9 @@ class Marketplace extends Resource
* 'country_code' => 'USA' * 'country_code' => 'USA'
* ) * )
* </code> * </code>
* *
* For a business the array looks like this: * For a business the array looks like this:
* *
* <code> * <code>
* array( * array(
* 'type' => 'business', * 'type' => 'business',
@ -200,10 +233,10 @@ class Marketplace extends Resource
* ) * )
* ) * )
* </code> * </code>
* *
* In some cases the identity of the merchant, person or business, cannot * In some cases the identity of the merchant, person or business, cannot
* be verified in which case a \Balanced\Exceptions\HTTPError is thrown: * be verified in which case a \Balanced\Exceptions\HTTPError is thrown:
* *
* <code> * <code>
* $identity = array( * $identity = array(
* 'type' => 'business', * 'type' => 'business',
@ -223,7 +256,7 @@ class Marketplace extends Resource
* 'country_code' => 'USA', * 'country_code' => 'USA',
* ), * ),
* ); * );
* *
* try { * try {
* $merchant = \Balanced\Marketplace::mine()->createMerchant( * $merchant = \Balanced\Marketplace::mine()->createMerchant(
* 'merchant@example.com', * 'merchant@example.com',
@ -231,15 +264,15 @@ class Marketplace extends Resource
* ); * );
* catch (\Balanced\Exceptions\HTTPError $e) { * catch (\Balanced\Exceptions\HTTPError $e) {
* if ($e->code != 300) { * if ($e->code != 300) {
* throw $e; * throw $e;
* } * }
* print e->response->header['Location'] // this is where merchant must signup * print e->response->header['Location'] // this is where merchant must signup
* } * }
* </code> * </code>
* *
* Once the merchant has completed signup you can use the resulting URI to * Once the merchant has completed signup you can use the resulting URI to
* create an account for them on your marketplace: * create an account for them on your marketplace:
* *
* <code> * <code>
* $merchant = self::$marketplace->createMerchant( * $merchant = self::$marketplace->createMerchant(
* 'merchant@example.com', * 'merchant@example.com',
@ -248,24 +281,23 @@ class Marketplace extends Resource
* $merchant_uri * $merchant_uri
* ); * );
* </coe> * </coe>
* *
* @param string email_address Email address. There can only be one account with this email address. * @param string email_address Optional email address. There can only be one account with this email address.
* @param array[string]mixed merchant Associative array describing the merchants identity. * @param array[string]mixed merchant Associative array describing the merchants identity.
* @param string $bank_account_uri Optional URI referencing a bank account to associate with this account. * @param string $bank_account_uri Optional URI referencing a bank account to associate with this account.
* @param string $merchant_uri URI of a merchant created via the redirection sign-up flow. * @param string $merchant_uri URI of a merchant created via the redirection sign-up flow.
* @param string $name Optional name of the merchant. * @param string $name Optional name of the merchant.
* @param array[string]string meta Optional metadata to associate with the account. * @param array[string]string meta Optional metadata to associate with the account.
* *
* @return \Balanced\Account * @return \Balanced\Account
*/ */
public function createMerchant( public function createMerchant(
$email_address, $email_address = null,
$merchant = null, $merchant = null,
$bank_account_uri = null, $bank_account_uri = null,
$merchant_uri = null, $merchant_uri = null,
$name = null, $name = null,
$meta = null $meta = null)
)
{ {
return $this->accounts->create(array( return $this->accounts->create(array(
'email_address' => $email_address, 'email_address' => $email_address,
@ -276,4 +308,18 @@ class Marketplace extends Resource
'meta' => $meta, 'meta' => $meta,
)); ));
} }
/*
* Create a callback.
*
* @param string url URL of callback.
*/
public function createCallback(
$url
)
{
return $this->callbacks->create(array(
'url' => $url
));
}
} }

View File

@ -2,8 +2,8 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use \RESTful\URISpec;
/** /**
* Represents a merchant identity. * Represents a merchant identity.
@ -14,7 +14,7 @@ use Balanced\Core\URISpec;
* *
* In some cases a merchant may need to be redirected to create a identity (e.g. the * In some cases a merchant may need to be redirected to create a identity (e.g. the
* information provided cannot be verified, more information is needed, etc). That * information provided cannot be verified, more information is needed, etc). That
* redirected signup results in a mechant_uri which is then asociated with an * redirected signup results in a merchant_uri which is then associated with an
* account on the marketplace via \Balanced\Marketplace::createMerchant. * account on the marketplace via \Balanced\Marketplace::createMerchant.
* *
* @see \Balanced\Marketplace * @see \Balanced\Marketplace
@ -41,7 +41,7 @@ class Merchant extends Resource
* assert($merchant->id == $owner_account->merchant->id); * assert($merchant->id == $owner_account->merchant->id);
* </code> * </code>
* *
* @throws \Balanced\Exceptions\NoResult * @throws \RESTful\Exceptions\NoResultFound
* @return \Balanced\Merchant * @return \Balanced\Merchant
*/ */
public static function me() public static function me()

View File

@ -2,11 +2,11 @@
namespace Balanced; namespace Balanced;
use Balanced\Core\Resource; use Balanced\Resource;
use Balanced\Core\URISpec; use \RESTful\URISpec;
/** /**
* Represents a refund of an account debit transaction. * Represents a refund of an account debit transaction.
* *
* You create these via Balanced\Debit::refund. * You create these via Balanced\Debit::refund.
* *
@ -35,16 +35,16 @@ use Balanced\Core\URISpec;
* 'my_id': '123123' * 'my_id': '123123'
* ) * )
* ); * );
* </code> * </code>
*/ */
class Refund extends Resource class Refund extends Resource
{ {
protected static $_uri_spec = null; protected static $_uri_spec = null;
public static function init() public static function init()
{ {
self::$_uri_spec = new URISpec('refunds', 'id'); self::$_uri_spec = new URISpec('refunds', 'id');
self::$_registry->add(get_called_class()); self::$_registry->add(get_called_class());
} }
} }

View File

@ -0,0 +1,48 @@
<?php
namespace Balanced;
use Balanced\Errors\Error;
use RESTful\Exceptions\HTTPError;
class Resource extends \RESTful\Resource
{
public static $fields, $f;
protected static $_client, $_registry, $_uri_spec;
public static function init()
{
self::$_client = new \RESTful\Client('\Balanced\Settings', null, __NAMESPACE__ .'\Resource::convertError');
self::$_registry = new \RESTful\Registry();
self::$f = self::$fields = new \RESTful\Fields();
}
public static function convertError($response)
{
if (property_exists($response->body, 'category_code') &&
array_key_exists($response->body->category_code, Error::$codes))
$error = new Error::$codes[$response->body->category_code]($response);
else
$error = new HTTPError($response);
return $error;
}
public static function getClient()
{
$class = get_called_class();
return $class::$_client;
}
public static function getRegistry()
{
$class = get_called_class();
return $class::$_registry;
}
public static function getURISpec()
{
$class = get_called_class();
return $class::$_uri_spec;
}
}

View File

@ -2,40 +2,42 @@
namespace Balanced; namespace Balanced;
/** /**
* Configurable settings. * Configurable settings.
* *
* You can either set these settings individually: * You can either set these settings individually:
* *
* <code> * <code>
* \Balanced\Settngs::api_key = 'my-api-key-secret'; * \Balanced\Settngs::api_key = 'my-api-key-secret';
* </code> * </code>
* *
* or all at once: * or all at once:
* *
* <code> * <code>
* \Balanced\Settngs::configure( * \Balanced\Settngs::configure(
* 'https://api.balancedpayments.com', * 'https://api.balancedpayments.com',
* 'my-api-key-secret' * 'my-api-key-secret'
* ); * );
* </code> * </code>
*/ */
class Settings class Settings
{ {
const VERSION = '0.6.6'; const VERSION = '0.7.1';
public static $url_root = 'https://api.balancedpayments.com', public static $url_root = 'https://api.balancedpayments.com',
$api_key = null; $api_key = null,
$agent = 'balanced-php',
$version = Settings::VERSION;
/** /**
* Configure all settings. * Configure all settings.
* *
* @param string url_root The root (schema://hostname[:port]) to use when constructing api URLs. * @param string url_root The root (schema://hostname[:port]) to use when constructing api URLs.
* @param string api_key The api key secret to use for authenticating when talking to the api. If null then api usage is limited to uauthenticated endpoints. * @param string api_key The api key secret to use for authenticating when talking to the api. If null then api usage is limited to uauthenticated endpoints.
*/ */
public static function configure($url_root, $api_key) public static function configure($url_root, $api_key)
{ {
self::$url_root= $url_root; self::$url_root= $url_root;
self::$api_key = $api_key; self::$api_key = $api_key;
} }
} }

View File

@ -0,0 +1,44 @@
<?php
namespace RESTful;
/**
* Bootstrapper for RESTful does autoloading.
*/
class Bootstrap
{
const DIR_SEPARATOR = DIRECTORY_SEPARATOR;
const NAMESPACE_SEPARATOR = '\\';
public static $initialized = false;
public static function init()
{
spl_autoload_register(array('\RESTful\Bootstrap', 'autoload'));
}
public static function autoload($classname)
{
self::_autoload(dirname(dirname(__FILE__)), $classname);
}
public static function pharInit()
{
spl_autoload_register(array('\RESTful\Bootstrap', 'pharAutoload'));
}
public static function pharAutoload($classname)
{
self::_autoload('phar://restful.phar', $classname);
}
private static function _autoload($base, $classname)
{
$parts = explode(self::NAMESPACE_SEPARATOR, $classname);
$path = $base . self::DIR_SEPARATOR . implode(self::DIR_SEPARATOR, $parts) . '.php';
if (file_exists($path)) {
require_once($path);
}
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace RESTful;
use RESTful\Exceptions\HTTPError;
use RESTful\Settings;
class Client
{
public function __construct($settings_class, $request_class = null, $convert_error = null)
{
$this->request_class = $request_class == null ? '\Httpful\Request' : $request_class;
$this->settings_class = $settings_class;
$this->convert_error = $convert_error;
}
public function get($uri)
{
$settings_class = $this->settings_class;
$url = $settings_class::$url_root . $uri;
$request_class = $this->request_class;
$request = $request_class::get($url);
return $this->_op($request);
}
public function post($uri, $payload)
{
$settings_class = $this->settings_class;
$url = $settings_class::$url_root . $uri;
$request_class = $this->request_class;
$request = $request_class::post($url, $payload, 'json');
return $this->_op($request);
}
public function put($uri, $payload)
{
$settings_class = $this->settings_class;
$url = $settings_class::$url_root . $uri;
$request_class = $this->request_class;
$request = $request_class::put($url, $payload, 'json');
return $this->_op($request);
}
public function delete($uri)
{
$settings_class = $this->settings_class;
$url = $settings_class::$url_root . $uri;
$request_class = $this->request_class;
$request = $request_class::delete($url);
return $this->_op($request);
}
private function _op($request)
{
$settings_class = $this->settings_class;
$user_agent = $settings_class::$agent . '/' . $settings_class::$version;
$request->headers['User-Agent'] = $user_agent;
if ($settings_class::$api_key != null) {
$request = $request->authenticateWith($settings_class::$api_key, '');
}
$request->expects('json');
$response = $request->sendIt();
if ($response->hasErrors() || $response->code == 300) {
if ($this->convert_error != null) {
$error = call_user_func($this->convert_error, $response);
} else {
$error = new HTTPError($response);
}
throw $error;
}
return $response;
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
namespace Balanced\Core; namespace RESTful;
class Collection extends Itemization class Collection extends Itemization
{ {
@ -9,40 +9,41 @@ class Collection extends Itemization
parent::__construct($resource, $uri, $data); parent::__construct($resource, $uri, $data);
$this->_parseUri(); $this->_parseUri();
} }
private function _parseUri() private function _parseUri()
{ {
$parsed = parse_url($this->uri); $parsed = parse_url($this->uri);
$this->_uri = $parsed['path']; $this->_uri = $parsed['path'];
if (array_key_exists('query', $parsed)) { if (array_key_exists('query', $parsed)) {
foreach (explode('&', $parsed['query']) as $param) { foreach (explode('&', $parsed['query']) as $param) {
$param = explode('=', $param); $param = explode('=', $param);
$key = urldecode($param[0]); $key = urldecode($param[0]);
$val = (count($param) == 1) ? null : urldecode($param[1]); $val = (count($param) == 1) ? null : urldecode($param[1]);
// size // size
if ($key == 'limit') { if ($key == 'limit') {
$this->_size = $val; $this->_size = $val;
} }
} }
} }
} }
public function create($payload) public function create($payload)
{ {
$class = $this->resource; $class = $this->resource;
$client = $class::getClient(); $client = $class::getClient();
$response = $client->post($this->uri, $payload); $response = $client->post($this->uri, $payload);
return new $this->resource($response->body); return new $this->resource($response->body);
} }
public function query() public function query()
{ {
return new Query($this->resource, $this->uri); return new Query($this->resource, $this->uri);
} }
public function paginate() public function paginate()
{ {
return new Pagination($this->resource, $this->uri); return new Pagination($this->resource, $this->uri);
} }
} }

View File

@ -0,0 +1,10 @@
<?php
namespace RESTful\Exceptions;
/**
* Base class for all RESTful\Exceptions.
*/
class Base extends \Exception
{
}

View File

@ -1,27 +1,28 @@
<?php <?php
namespace Balanced\Exceptions; namespace RESTful\Exceptions;
/** /**
* Indicates an HTTP level error has occured. The underlying HTTP response is * Indicates an HTTP level error has occurred. The underlying HTTP response is
* stored as response member. The response payload fields if any are stored as * stored as response member. The response payload fields if any are stored as
* members of the same name. * members of the same name.
* *
* @see \Httpful\Response * @see \Httpful\Response
*/ */
class HTTPError extends Base class HTTPError extends Base
{ {
public $response; public $response;
public function __construct($response) public function __construct($response)
{ {
$this->response = $response; $this->response = $response;
$this->_objectify($this->response->body); $this->_objectify($this->response->body);
} }
protected function _objectify($fields) protected function _objectify($fields)
{ {
foreach ($fields as $key => $val) foreach ($fields as $key => $val) {
$this->$key = $val; $this->$key = $val;
}
} }
} }

View File

@ -1,11 +1,11 @@
<?php <?php
namespace Balanced\Exceptions; namespace RESTful\Exceptions;
/** /**
* Indicates that a query unexpectedly returned multiple results when at most * Indicates that a query unexpectedly returned multiple results when at most
* one was expected. * one was expected.
*/ */
class MultipleResultsFound extends Base class MultipleResultsFound extends Base
{ {
} }

View File

@ -0,0 +1,10 @@
<?php
namespace RESTful\Exceptions;
/**
* Indicates that a query unexpectedly returned no results.
*/
class NoResultFound extends Base
{
}

View File

@ -0,0 +1,85 @@
<?php
namespace RESTful;
class Field
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
public function __get($name)
{
return new Field($this->name . '.' . $name);
}
public function in($vals)
{
return new FilterExpression($this->name, 'in', $vals, '!in');
}
public function startswith($prefix)
{
if (!is_string($prefix)) {
throw new \InvalidArgumentException('"startswith" prefix must be a string');
}
return new FilterExpression($this->name, 'contains', $prefix);
}
public function endswith($suffix)
{
if (!is_string($suffix)) {
throw new \InvalidArgumentException('"endswith" suffix must be a string');
}
return new FilterExpression($this->name, 'contains', $suffix);
}
public function contains($fragment)
{
if (!is_string($fragment)) {
throw new \InvalidArgumentException('"contains" fragment must be a string');
}
return new FilterExpression($this->name, 'contains', $fragment, '!contains');
}
public function eq($val)
{
return new FilterExpression($this->name, '=', $val, '!eq');
}
public function lt($val)
{
return new FilterExpression($this->name, '<', $val, '>=');
}
public function lte($val)
{
return new FilterExpression($this->name, '<=', $val, '>');
}
public function gt($val)
{
return new FilterExpression($this->name, '>', $val, '<=');
}
public function gte($val)
{
return new FilterExpression($this->name, '>=', $val, '<');
}
public function asc()
{
return new SortExpression($this->name, true);
}
public function desc()
{
return new SortExpression($this->name, false);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace RESTful;
class Fields
{
public function __get($name)
{
return new Field($name);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace RESTful;
class FilterExpression
{
public $field,
$op,
$val,
$not_op;
public function __construct($field, $op, $val, $not_op = null)
{
$this->field = $field;
$this->op = $op;
$this->val = $val;
$this->not_op = $not_op;
}
public function not()
{
if (null === $this->not_op) {
throw new \LogicException(sprintf('Filter cannot be inverted'));
}
$temp = $this->op;
$this->op = $this->not_op;
$this->not_op = $temp;
return $this;
}
}

View File

@ -1,142 +1,99 @@
<?php <?php
namespace Balanced\Core; namespace RESTful;
class ItemizationIterator implements \Iterator
{
protected $_page,
$_offset = 0;
public function __construct($resource, $uri, $data = null)
{
$this->_page = new Page($resource, $uri, $data);
}
// Iterator
public function current()
{
return $this->_page->items[$this->_offset];
}
public function key()
{
return $this->_page->offset + $this->_offset;
}
public function next()
{
$this->_offset += 1;
if ($this->_offset >= count($this->_page->items)) {
$this->_offset = 0;
$this->_page = $this->_page->next();
}
}
public function rewind()
{
$this->_page = $this->_page->first();
$this->_offset = 0;
}
public function valid()
{
return ($this->_page != null &&
$this->_offset < count($this->_page->items));
}
}
class Itemization implements \IteratorAggregate, \ArrayAccess class Itemization implements \IteratorAggregate, \ArrayAccess
{ {
public $resource, public $resource,
$uri; $uri;
protected $_page, protected $_page,
$_offset = 0, $_offset = 0,
$_size=25; $_size = 25;
public function __construct($resource, $uri, $data = null) public function __construct($resource, $uri, $data = null)
{
$this->resource = $resource;
$this->uri = $uri;
if ($data != null)
$this->_page = new Page($resource, $uri, $data);
else
$this->_page = null;
}
protected function _getPage($offset = null)
{
if ($this->_page == null) {
$this->_offset = ($offset == null) ? 0 : $offset * $this->_size;
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
}
else if ($offset != null) {
$offset = $offset * $this->_size;
if ($offset != $this->_offset) {
$this->_offset = $offset;
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
}
}
return $this->_page;
}
protected function _getItem($offset)
{ {
$page_offset = floor($offset/$this->_size); $this->resource = $resource;
$this->uri = $uri;
if ($data != null) {
$this->_page = new Page($resource, $uri, $data);
} else {
$this->_page = null;
}
}
protected function _getPage($offset = null)
{
if ($this->_page == null) {
$this->_offset = ($offset == null) ? 0 : $offset * $this->_size;
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
} elseif ($offset != null) {
$offset = $offset * $this->_size;
if ($offset != $this->_offset) {
$this->_offset = $offset;
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
}
}
return $this->_page;
}
protected function _getItem($offset)
{
$page_offset = floor($offset / $this->_size);
$page = $this->_getPage($page_offset); $page = $this->_getPage($page_offset);
return $page->items[$offset - $page->offset];
return $page->items[$offset - $page->offset];
} }
public function total() public function total()
{ {
return $this->_getPage()->total; return $this->_getPage()->total;
} }
protected function _buildUri($offset = null) protected function _buildUri($offset = null)
{ {
# TODO: hacky but works for now # TODO: hacky but works for now
$offset = ($offset == null) ? $this->_offset : $offset; $offset = ($offset == null) ? $this->_offset : $offset;
if (strpos($this->uri, '?') === false) if (strpos($this->uri, '?') === false) {
$uri = $this->uri . '?'; $uri = $this->uri . '?';
else } else {
$uri = $this->uri . '&'; $uri = $this->uri . '&';
$uri = $uri . 'offset=' . strval($offset); }
$uri = $uri . 'offset=' . strval($offset);
return $uri; return $uri;
} }
// IteratorAggregate // IteratorAggregate
public function getIterator() public function getIterator()
{ {
$uri = $this->_buildUri($offset = 0); $uri = $this->_buildUri($offset = 0);
$uri = $this->_buildUri($offset = 0); $uri = $this->_buildUri($offset = 0);
return new ItemizationIterator($this->resource, $uri);
return new ItemizationIterator($this->resource, $uri);
} }
// ArrayAccess // ArrayAccess
public function offsetSet($offset, $value)
public function offsetSet($offset, $value)
{ {
throw new BadMethodCallException(get_class($this) . ' array access is read-only'); throw new \BadMethodCallException(get_class($this) . ' array access is read-only');
} }
public function offsetExists($offset) public function offsetExists($offset)
{
return (0 <= $offset && $offset < $this->total());
}
public function offsetUnset($offset)
{ {
throw new BadMethodCallException(get_class($this) . ' array access is read-only'); return (0 <= $offset && $offset < $this->total());
} }
public function offsetGet($offset) public function offsetUnset($offset)
{ {
return $this->_getItem($offset); throw new \BadMethodCallException(get_class($this) . ' array access is read-only');
} }
public function offsetGet($offset)
{
return $this->_getItem($offset);
}
} }

View File

@ -0,0 +1,45 @@
<?php
namespace RESTful;
class ItemizationIterator implements \Iterator
{
protected $_page,
$_offset = 0;
public function __construct($resource, $uri, $data = null)
{
$this->_page = new Page($resource, $uri, $data);
}
// Iterator
public function current()
{
return $this->_page->items[$this->_offset];
}
public function key()
{
return $this->_page->offset + $this->_offset;
}
public function next()
{
$this->_offset += 1;
if ($this->_offset >= count($this->_page->items)) {
$this->_offset = 0;
$this->_page = $this->_page->next();
}
}
public function rewind()
{
$this->_page = $this->_page->first();
$this->_offset = 0;
}
public function valid()
{
return ($this->_page != null && $this->_offset < count($this->_page->items));
}
}

View File

@ -1,70 +1,72 @@
<?php <?php
namespace Balanced\Core; namespace RESTful;
class Page class Page
{ {
public $resource, public $resource,
$total, $total,
$items, $items,
$offset, $offset,
$limit; $limit;
private $_first_uri, private $_first_uri,
$_previous_uri, $_previous_uri,
$_next_uri, $_next_uri,
$_last_uri; $_last_uri;
public function __construct($resource, $uri, $data = null) public function __construct($resource, $uri, $data = null)
{ {
$this->resource = $resource; $this->resource = $resource;
if ($data == null) { if ($data == null) {
$client = $resource::getClient(); $client = $resource::getClient();
$data = $client->get($uri)->body; $data = $client->get($uri)->body;
} }
$this->total = $data->total; $this->total = $data->total;
$this->items = array_map( $this->items = array_map(
function($x) use ($resource) { function ($x) use ($resource) {
return new $resource($x); return new $resource($x);
}, },
$data->items); $data->items);
$this->offset = $data->offset; $this->offset = $data->offset;
$this->limit = $data->limit; $this->limit = $data->limit;
$this->_first_uri = $data->first_uri; $this->_first_uri = property_exists($data, 'first_uri') ? $data->first_uri : null;
$this->_previous_uri = $data->previous_uri; $this->_previous_uri = property_exists($data, 'previous_uri') ? $data->previous_uri : null;
$this->_next_uri = $data->next_uri; $this->_next_uri = property_exists($data, 'next_uri') ? $data->next_uri : null;
$this->_last_uri = $data->last_uri; $this->_last_uri = property_exists($data, 'last_uri') ? $data->last_uri : null;
} }
public function first() public function first()
{ {
return new Page($this->resource, $this->_first_uri); return new Page($this->resource, $this->_first_uri);
} }
public function next() public function next()
{ {
if (!$this->hasNext()) if (!$this->hasNext()) {
return null; return null;
return new Page($this->resource, $this->_next_uri); }
}
return new Page($this->resource, $this->_next_uri);
public function hasNext() }
{
return $this->_next_uri != null; public function hasNext()
} {
return $this->_next_uri != null;
public function previous() }
{
return new Page($this->resource, $this->_previous_uri); public function previous()
} {
return new Page($this->resource, $this->_previous_uri);
public function hasPrevious() }
{
return $this->_previous_uri != null; public function hasPrevious()
} {
return $this->_previous_uri != null;
public function last() }
{
return new Page($this->resource, $this->_last_uri); public function last()
} {
} return new Page($this->resource, $this->_last_uri);
}
}

View File

@ -1,125 +1,90 @@
<?php <?php
namespace Balanced\Core; namespace RESTful;
class Pagination implements \IteratorAggregate, \ArrayAccess
class PaginationIterator implements \Iterator
{
public function __construct($resource, $uri, $data = null)
{
$this->_page = new Page($resource, $uri, $data);
}
// Iterator
public function current()
{
return $this->_page;
}
public function key()
{
return $this->_page->index;
}
public function next()
{
$this->_page = $this->_page->next();
}
public function rewind()
{
$this->_page = $this->_page->first();
}
public function valid()
{
return $this->_page != null;
}
}
class Pagination implements \IteratorAggregate, \ArrayAccess
{ {
public $resource, public $resource,
$uri; $uri;
protected $_page, protected $_page,
$_offset=0, $_offset = 0,
$_size=25; $_size = 25;
public function __construct($resource, $uri, $data = null) public function __construct($resource, $uri, $data = null)
{ {
$this->resource = $resource; $this->resource = $resource;
$this->uri = $uri; $this->uri = $uri;
if ($data != null) if ($data != null) {
$this->_page = new Page($resource, $uri, $data); $this->_page = new Page($resource, $uri, $data);
else } else {
$this->_page = null; $this->_page = null;
}
} }
protected function _getPage($offset = null) protected function _getPage($offset = null)
{ {
if ($this->_page == null) { if ($this->_page == null) {
$this->_offset = ($offset == null) ? 0 : $offset * $this->_size; $this->_offset = ($offset == null) ? 0 : $offset * $this->_size;
$uri = $this->_buildUri(); $uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri); $this->_page = new Page($this->resource, $uri);
} } elseif ($offset != null) {
else if ($offset != null) {
$offset = $offset * $this->_size; $offset = $offset * $this->_size;
if ($offset != $this->_offset) { if ($offset != $this->_offset) {
$this->_offset = $offset; $this->_offset = $offset;
$uri = $this->_buildUri(); $uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri); $this->_page = new Page($this->resource, $uri);
} }
} }
return $this->_page; return $this->_page;
}
public function total()
{
return floor($this->_getPage()->total / $this->_size);
} }
public function total()
{
return floor($this->_getPage()->total / $this->_size);
}
protected function _buildUri($offset = null) protected function _buildUri($offset = null)
{ {
# TODO: hacky but works for now # TODO: hacky but works for now
$offset = ($offset == null) ? $this->_offset : $offset; $offset = ($offset == null) ? $this->_offset : $offset;
if (strpos($this->uri, '?') === false) if (strpos($this->uri, '?') === false) {
$uri = $this->uri . '?'; $uri = $this->uri . '?';
else } else {
$uri = $this->uri . '&'; $uri = $this->uri . '&';
}
$uri = $uri . 'offset=' . strval($offset); $uri = $uri . 'offset=' . strval($offset);
return $uri; return $uri;
} }
// IteratorAggregate // IteratorAggregate
public function getIterator()
public function getIterator()
{ {
$uri = $this->_buildUri($offset = 0); $uri = $this->_buildUri($offset = 0);
return new PaginationIterator($this->resource, $uri);
return new PaginationIterator($this->resource, $uri);
} }
// ArrayAccess // ArrayAccess
public function offsetSet($offset, $value) public function offsetSet($offset, $value)
{ {
throw new BadMethodCallException(get_class($this) . ' array access is read-only'); throw new \BadMethodCallException(get_class($this) . ' array access is read-only');
} }
public function offsetExists($offset) public function offsetExists($offset)
{ {
return (0 <= $offset && $offset < $this->total()); return (0 <= $offset && $offset < $this->total());
} }
public function offsetUnset($offset) public function offsetUnset($offset)
{ {
throw new BadMethodCallException(get_class($this) . ' array access is read-only'); throw new \BadMethodCallException(get_class($this) . ' array access is read-only');
} }
public function offsetGet($offset) public function offsetGet($offset)
{ {
return $this->_getPage($offset); return $this->_getPage($offset);
} }
} }

View File

@ -0,0 +1,37 @@
<?php
namespace RESTful;
class PaginationIterator implements \Iterator
{
public function __construct($resource, $uri, $data = null)
{
$this->_page = new Page($resource, $uri, $data);
}
// Iterator
public function current()
{
return $this->_page;
}
public function key()
{
return $this->_page->index;
}
public function next()
{
$this->_page = $this->_page->next();
}
public function rewind()
{
$this->_page = $this->_page->first();
}
public function valid()
{
return $this->_page != null;
}
}

View File

@ -1,149 +1,161 @@
<?php <?php
namespace Balanced\Core; namespace RESTful;
use Balanced\Exceptions\NoResultFound; use RESTful\Exceptions\NoResultFound;
use Balanced\Exceptions\MultipleResultsFound; use RESTful\Exceptions\MultipleResultsFound;
class Query extends Itemization
class Query extends Itemization {
{
public $filters = array(), public $filters = array(),
$sorts = array(), $sorts = array(),
$size; $size;
public function __construct($resource, $uri) public function __construct($resource, $uri)
{ {
parent::__construct($resource, $uri); parent::__construct($resource, $uri);
$this->size = $this->_size; $this->size = $this->_size;
$this->_parseUri($uri); $this->_parseUri($uri);
} }
private function _parseUri($uri) private function _parseUri($uri)
{
$parsed = parse_url($uri);
$this->uri = $parsed['path'];
if (array_key_exists('query', $parsed)) {
foreach (explode('&', $parsed['query']) as $param) {
$param = explode('=', $param);
$key = urldecode($param[0]);
$val = (count($param) == 1) ? null : urldecode($param[1]);
// limit
if ($key == 'limit') {
$this->size = $this->_size = $val;
}
// sorts
else if ($key == 'sort') {
array_push($this->sorts, $val);
}
// everything else
else {
if (!array_key_exists($key, $this->filters))
$this->filters[$key] = array();
if (!is_array($val))
$val = array($val);
$this->filters[$key] = array_merge($this->filters[$key], $val);
}
}
}
}
protected function _buildUri($offset = null)
{
// params
$params = array_merge(
$this->filters,
array(
'sort' => $this->sorts,
'limit' => $this->_size,
'offset' => ($offset == null) ? $this->_offset : $offset));
$getSingle = function ($v) {
if (is_array($v) && count($v) == 1)
return $v[0];
return $v;
};
$params = array_map($getSingle, $params);
// url encode params
// NOTE: http://stackoverflow.com/a/8171667/1339571
$qs = http_build_query($params);
$qs = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', $qs);
return $this->uri . '?' . $qs;
}
private function _reset()
{
$this->_page = null;
}
public function filter($expression)
{ {
if ($expression->op == '=') $parsed = parse_url($uri);
$field = $expression->field; $this->uri = $parsed['path'];
else if (array_key_exists('query', $parsed)) {
$field = $expression->field . '[' . $expression->op . ']'; foreach (explode('&', $parsed['query']) as $param) {
if (is_array($expression->val)) $param = explode('=', $param);
$val = implode(',', $expression->val); $key = urldecode($param[0]);
else $val = (count($param) == 1) ? null : urldecode($param[1]);
$val = $expression->val;
if (!array_key_exists($field, $this->filters)) // limit
$this->filters[$field] = array(); if ($key == 'limit') {
array_push($this->filters[$field], $val); $this->size = $this->_size = $val;
$this->_reset(); } // sorts
return $this; else if ($key == 'sort') {
} array_push($this->sorts, $val);
} // everything else
public function sort($expression) else {
{ if (!array_key_exists($key, $this->filters)) {
$dir = $expression->ascending ? 'asc' : 'desc'; $this->filters[$key] = array();
array_push($this->sorts, $expression->field . ',' . $dir); }
$this->_reset(); if (!is_array($val)) {
return $this; $val = array($val);
} }
$this->filters[$key] = array_merge($this->filters[$key], $val);
public function limit($limit) }
{ }
$this->size = $this->_size = $limit;
$this->_reset();
return $this;
}
public function all()
{
$items = array();
foreach($this as $item) {
array_push($items, $item);
} }
return $items;
}
public function first()
{
$prev_size = $this->_size;
$this->_size = 1;
$page = new Page($this->resource, $this->_buildUri());
$this->_size = $prev_size;
$item = count($page->items) != 0 ? $page->items[0] : null;
return $item;
}
public function one()
{
$prev_size = $this->_size;
$this->_size = 2;
$page = new Page($this->resource, $this->_buildUri());
$this->_size = $prev_size;
if (count($page->items) == 1)
return $page->items[0];
if (count($page->items) == 0)
throw new NoResultFound();
throw new MultipleResultsFound();
} }
public function paginate() protected function _buildUri($offset = null)
{ {
return new Pagination($this->resource, $this->_buildUri()); // params
} $params = array_merge(
$this->filters,
array(
'sort' => $this->sorts,
'limit' => $this->_size,
'offset' => ($offset == null) ? $this->_offset : $offset
)
);
$getSingle = function ($v) {
if (is_array($v) && count($v) == 1)
return $v[0];
return $v;
};
$params = array_map($getSingle, $params);
// url encode params
// NOTE: http://stackoverflow.com/a/8171667/1339571
$qs = http_build_query($params);
$qs = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', $qs);
return $this->uri . '?' . $qs;
}
private function _reset()
{
$this->_page = null;
}
public function filter($expression)
{
if ($expression->op == '=') {
$field = $expression->field;
} else {
$field = $expression->field . '[' . $expression->op . ']';
}
if (is_array($expression->val)) {
$val = implode(',', $expression->val);
} else {
$val = $expression->val;
}
if (!array_key_exists($field, $this->filters)) {
$this->filters[$field] = array();
}
array_push($this->filters[$field], $val);
$this->_reset();
return $this;
}
public function sort($expression)
{
$dir = $expression->ascending ? 'asc' : 'desc';
array_push($this->sorts, $expression->field . ',' . $dir);
$this->_reset();
return $this;
}
public function limit($limit)
{
$this->size = $this->_size = $limit;
$this->_reset();
return $this;
}
public function all()
{
$items = array();
foreach ($this as $item) {
array_push($items, $item);
}
return $items;
}
public function first()
{
$prev_size = $this->_size;
$this->_size = 1;
$page = new Page($this->resource, $this->_buildUri());
$this->_size = $prev_size;
$item = count($page->items) != 0 ? $page->items[0] : null;
return $item;
}
public function one()
{
$prev_size = $this->_size;
$this->_size = 2;
$page = new Page($this->resource, $this->_buildUri());
$this->_size = $prev_size;
if (count($page->items) == 1) {
return $page->items[0];
}
if (count($page->items) == 0) {
throw new NoResultFound();
}
throw new MultipleResultsFound();
}
public function paginate()
{
return new Pagination($this->resource, $this->_buildUri());
}
} }

View File

@ -1,24 +1,29 @@
<?php <?php
namespace Balanced\Core; namespace RESTful;
class Registry class Registry
{ {
protected $_resources = array(); protected $_resources = array();
public function add($resource) { public function add($resource)
{
array_push($this->_resources, $resource); array_push($this->_resources, $resource);
} }
public function match($uri) { public function match($uri)
{
foreach ($this->_resources as $resource) { foreach ($this->_resources as $resource) {
$spec = $resource::getURISpec(); $spec = $resource::getURISpec();
$result = $spec->match($uri); $result = $spec->match($uri);
if ($result == null) if ($result == null) {
continue; continue;
}
$result['class'] = $resource; $result['class'] = $resource;
return $result;
return $result;
} }
return null;
return null;
} }
} }

View File

@ -0,0 +1,205 @@
<?php
namespace RESTful;
abstract class Resource
{
protected $_collection_uris,
$_member_uris;
public static function getClient()
{
$class = get_called_class();
return $class::$_client;
}
public static function getRegistry()
{
$class = get_called_class();
return $class::$_registry;
}
public static function getURISpec()
{
$class = get_called_class();
return $class::$_uri_spec;
}
public function __construct($fields = null)
{
if ($fields == null) {
$fields = array();
}
$this->_objectify($fields);
}
public function __get($name)
{
// collection uri
if (array_key_exists($name, $this->_collection_uris)) {
$result = $this->_collection_uris[$name];
$this->$name = new Collection($result['class'], $result['uri']);
return $this->$name;
} // member uri
else if (array_key_exists($name, $this->_member_uris)) {
$result = $this->$_collection_uris[$name];
$response = self::getClient() . get($result['uri']);
$class = $result['class'];
$this->$name = new $class($response->body);
return $this->$name;
}
// unknown
$trace = debug_backtrace();
trigger_error(
sprintf('Undefined property via __get(): %s in %s on line %s', $name, $trace[0]['file'], $trace[0]['line']),
E_USER_NOTICE
);
return null;
}
public function __isset($name)
{
if (array_key_exists($name, $this->_collection_uris) || array_key_exists($name, $this->_member_uris)) {
return true;
}
return false;
}
protected function _objectify($fields)
{
// initialize uris
$this->_collection_uris = array();
$this->_member_uris = array();
foreach ($fields as $key => $val) {
// nested uri
if ((strlen($key) - 3) == strrpos($key, 'uri', 0) && $key != 'uri') {
$result = self::getRegistry()->match($val);
if ($result != null) {
$name = substr($key, 0, -4);
$class = $result['class'];
if ($result['collection']) {
$this->_collection_uris[$name] = array(
'class' => $class,
'uri' => $val,
);
} else {
$this->_member_uris[$name] = array(
'class' => $class,
'uri' => $val,
);
}
continue;
}
} elseif (is_object($val) && property_exists($val, 'uri')) {
// nested
$result = self::getRegistry()->match($val->uri);
if ($result != null) {
$class = $result['class'];
if ($result['collection']) {
$this->$key = new Collection($class, $val['uri'], $val);
} else {
$this->$key = new $class($val);
}
continue;
}
} elseif (is_array($val) && array_key_exists('uri', $val)) {
$result = self::getRegistry()->match($val['uri']);
if ($result != null) {
$class = $result['class'];
if ($result['collection']) {
$this->$key = new Collection($class, $val['uri'], $val);
} else {
$this->$key = new $class($val);
}
continue;
}
}
// default
$this->$key = $val;
}
}
public static function query()
{
$uri_spec = self::getURISpec();
if ($uri_spec == null || $uri_spec->collection_uri == null) {
$msg = sprintf('Cannot directly query %s resources', get_called_class());
throw new \LogicException($msg);
}
return new Query(get_called_class(), $uri_spec->collection_uri);
}
public static function get($uri)
{
# id
if (strncmp($uri, '/', 1)) {
$uri_spec = self::getURISpec();
if ($uri_spec == null || $uri_spec->collection_uri == null) {
$msg = sprintf('Cannot get %s resources by id %s', $class, $uri);
throw new \LogicException($msg);
}
$uri = $uri_spec->collection_uri . '/' . $uri;
}
$response = self::getClient()->get($uri);
$class = get_called_class();
return new $class($response->body);
}
public function save()
{
// payload
$payload = array();
foreach ($this as $key => $val) {
if ($key[0] == '_' || is_object($val)) {
continue;
}
$payload[$key] = $val;
}
// update
if (array_key_exists('uri', $payload)) {
$uri = $payload['uri'];
unset($payload['uri']);
$response = self::getClient()->put($uri, $payload);
} else {
// create
$class = get_class($this);
if ($class::$_uri_spec == null || $class::$_uri_spec->collection_uri == null) {
$msg = sprintf('Cannot directly create %s resources', $class);
throw new \LogicException($msg);
}
$response = self::getClient()->post($class::$_uri_spec->collection_uri, $payload);
}
// re-objectify
foreach ($this as $key => $val) {
unset($this->$key);
}
$this->_objectify($response->body);
return $this;
}
public function delete()
{
self::getClient()->delete($this->uri);
return $this;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace RESTful;
/**
* Settings.
*
*/
class Settings
{
const VERSION = '0.1.7';
}

View File

@ -0,0 +1,15 @@
<?php
namespace RESTful;
class SortExpression
{
public $name,
$ascending;
public function __construct($field, $ascending = true)
{
$this->field = $field;
$this->ascending = $ascending;
}
}

View File

@ -1,48 +1,58 @@
<?php <?php
namespace Balanced\Core; namespace RESTful;
class URISpec class URISpec
{ {
public $collection_uri = null, public $collection_uri = null,
$name, $name,
$idNames; $idNames;
public function __construct($name, $idNames, $root = null) public function __construct($name, $idNames, $root = null)
{ {
$this->name = $name; $this->name = $name;
if (!is_array($idNames)) if (!is_array($idNames)) {
$idNames = array($idNames); $idNames = array($idNames);
}
$this->idNames = $idNames; $this->idNames = $idNames;
if ($root != null) if ($root != null) {
$this->collection_uri = $root . '/' . $name; if ($root == '' || substr($root, -1) == '/') {
$this->collection_uri = $root . $name;
} else {
$this->collection_uri = $root . '/' . $name;
}
}
} }
public function match($uri) public function match($uri)
{ {
$parts = explode('/', $uri); $parts = explode('/', rtrim($uri, "/"));
// collection // collection
if ($parts[count($parts) - 1] == $this->name) if ($parts[count($parts) - 1] == $this->name) {
return array( return array(
'collection' => true, 'collection' => true,
); );
}
// non-member // non-member
if (count($parts) < count($this->idNames) + 1 || if (count($parts) < count($this->idNames) + 1 ||
$parts[count($parts) - 1 - count($this->idNames)] != $this->name) $parts[count($parts) - 1 - count($this->idNames)] != $this->name
) {
return null; return null;
}
// member // member
$ids = array_combine( $ids = array_combine(
$this->idNames, $this->idNames,
array_slice($parts, -count($this->idNames)) array_slice($parts, -count($this->idNames))
); );
$result = array( $result = array(
'collection' => false, 'collection' => false,
'ids' => $ids, 'ids' => $ids,
); );
return $result; return $result;
} }
} }