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

View File

@ -2,12 +2,12 @@
namespace Balanced;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
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:
*
@ -41,15 +41,15 @@ use Balanced\Settings;
* ->sort(\Balanced\APIKey::f->created_at->desc())
* ->first()
* ->delete();
* </code>
*/
class APIKey extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('api_keys', 'id', '/v1');
self::$_registry->add(get_called_class());
}
}
* </code>
*/
class APIKey extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('api_keys', 'id', '/v1');
self::$_registry->add(get_called_class());
}
}

View File

@ -2,8 +2,8 @@
namespace Balanced;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
use \RESTful\URISpec;
/**
* Represent a buyer or merchant account on a marketplace.
@ -94,25 +94,43 @@ class Account extends Resource
$appears_on_statement_as = null,
$description = null,
$meta = null,
$source = null)
$source = null,
$on_behalf_of = null)
{
if ($source == null)
if ($source == null) {
$source_uri = null;
else
$source_uri = is_string($source) ? $source : $source->uri;
} else if (is_string($source)) {
$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(
'amount' => $amount,
'appears_on_statement_as' => $appears_on_statement_as,
'description' => $description,
'meta' => $meta,
'source_uri' => $source_uri,
'on_behalf_of_uri' => $on_behalf_of_uri,
'appears_on_statement_as' => $appears_on_statement_as
));
}
/**
* 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
* 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.
*
* @return Balanced\Account
* @return \Balanced\Account
*/
public function addCard($card)
{
@ -164,9 +182,9 @@ class Account extends Resource
*
* @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.
* @return Balanced\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
*/
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.
*
* @return Balanced\Account
* @return \Balanced\Account
*/
public function promoteToMerchant($merchant)
{
$this->merchant = $merchant;
if (is_string($merchant))
$this->merchant_uri = $merchant;
else
$this->merchant = $merchant;
return $this->save();
}
}

View File

@ -2,8 +2,8 @@
namespace Balanced;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
use \RESTful\URISpec;
/**
* Represents an account bank account.
@ -41,20 +41,87 @@ class BankAccount extends Resource
self::$_registry->add(get_called_class());
}
public function credit(
$amount,
$description = null,
$meta = null,
$appears_on_statement_as = null)
/**
* Credit a bank account.
*
* @param int amount Amount to credit in USD pennies.
* @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) {
throw new \UnexpectedValueException('Bank account is not associated with an account.');
}
return $this->account->credit(
$amount,
$description,
$meta,
$this->uri,
$appears_on_statement_as);
if (!property_exists($this, 'account') || $this->account == null) {
$credit = $this->credits->create(array(
'amount' => $amount,
'description' => $description,
));
} else {
$credit = $this->account->credit(
$amount,
$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
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
{
const DIR_SEPARATOR = DIRECTORY_SEPARATOR;
const NAMESPACE_SEPARATOR = '\\';
const DIR_SEPARATOR = DIRECTORY_SEPARATOR;
const NAMESPACE_SEPARATOR = '\\';
public static $initialized = false;
public static function init()
{
spl_autoload_register(array('\Balanced\Bootstrap', 'autoload'));
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 $initialized = false;
public static function init()
{
spl_autoload_register(array('\Balanced\Bootstrap', 'autoload'));
self::initializeResources();
}
/**
* 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\Core\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();
self::$initialized = true;
}
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) {
if (!strncmp($classname, 'Balanced\Errors\\', strlen('Balanced\Errors\\'))) {
$classname = 'Balanced\Errors';
}
$parts = explode(self::NAMESPACE_SEPARATOR, $classname);
$path = $base . self::DIR_SEPARATOR. implode(self::DIR_SEPARATOR, $parts) . '.php';
if (file_exists($path)) {
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;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
use \RESTful\URISpec;
/**
* Represents an account card.
@ -30,32 +30,32 @@ use Balanced\Core\URISpec;
* ->one();
* $account->addCard($card->uri);
* </code>
*/
class Card extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('cards', 'id', '/v1');
self::$_registry->add(get_called_class());
*/
class Card extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('cards', 'id', '/v1');
self::$_registry->add(get_called_class());
}
public function debit(
$amount,
$appears_on_statement_as = null,
$description = null,
$meta = null,
public function debit(
$amount,
$appears_on_statement_as = null,
$description = null,
$meta = null,
$source = null)
{
if ($this->account == null) {
throw new \UnexpectedValueException('Card is not associated with an account.');
}
return $this->account->debit(
$amount,
return $this->account->debit(
$amount,
$appears_on_statement_as,
$description,
$meta,
$meta,
$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;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
use \RESTful\URISpec;
/**
/**
* Represents an account credit transaction.
*
* You create these using Balanced\Account::credit.
@ -26,16 +26,50 @@ use Balanced\Core\URISpec;
* 'my_id': '112233'
* )
* );
* </code>
*/
class Credit extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('credits', 'id');
self::$_registry->add(get_called_class());
}
}
* </code>
*/
class Credit extends Resource
{
protected static $_uri_spec = null;
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;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
use \RESTful\URISpec;
/**
/**
* Represents an account debit transaction.
*
* You create these using Balanced\Account::debit.
*
* <code>
* $marketplace = \Balanced\Marketplace::mine();
*
*
* $account = $marketplace
* ->accounts
* ->query()
* ->filter(Account::f->email_address->eq('buyer@example.com'))
* ->one();
*
*
* $debit = $account->debit(
* 100,
* 'how it appears on the statement',
@ -27,16 +27,16 @@ use Balanced\Core\URISpec;
* 'my_id': '443322'
* )
* );
* </code>
*/
class Debit extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('debits', 'id');
self::$_registry->add(get_called_class());
* </code>
*/
class Debit extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('debits', 'id');
self::$_registry->add(get_called_class());
}
/**
@ -56,9 +56,9 @@ class Debit extends Resource
$meta = null)
{
return $this->refunds->create(array(
'amount' => $amount,
'description' => $description,
'amount' => $amount,
'description' => $description,
'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;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
use \RESTful\URISpec;
/**
* 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
* 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.
*
* You create these using \Balanced\Account::hold.
@ -38,40 +38,40 @@ use Balanced\Core\URISpec;
*/
class Hold extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('holds', 'id');
self::$_registry->add(get_called_class());
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('holds', 'id');
self::$_registry->add(get_called_class());
}
/**
/**
** 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.
*
* @return Balanced\Hold
* @return \Balanced\Hold
*/
public function void()
{
$this->is_void = true;
$this->is_void = true;
return $this->save();
}
/**
/**
* 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.
*
* @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(
'hold_uri' => $this->uri,
'amount' => $amount,
));
return $this->debit;
return $this->debit;
}
}

View File

@ -2,60 +2,62 @@
namespace Balanced;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
use Balanced\Errors;
use Balanced\Account;
use \RESTful\URISpec;
/**
* Represents a marketplace.
*
*
* To get started you create an api key and then create a marketplace:
*
*
* <code>
* $api_key = new \Balanced\APIKey();
* $api_key->save();
* $secret = $api_key->secret // better save this somewhere
* print $secret;
* \Balanced\Settings::$api_key = $secret;
*
*
* $marketplace = new \Balanced\Marketplace();
* $marketplace->save();
* var_dump($marketplace);
* var_dump($marketplace);
* </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:
*
*
* <code>
* \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>
*/
class Marketplace extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('marketplaces', 'id', '/v1');
self::$_registry->add(get_called_class());
}
/**
* 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
*/
public static function mine()
{
return self::query()->one();
}
/**
* 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 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.
@ -64,7 +66,7 @@ class Marketplace extends Resource
* @param string security_code Card security code. Use null if it is no available.
* @param int expiration_month Expiration month.
* @param int expiration_year Expiration year.
*
*
* @return \Balanced\Card
*/
public function createCard(
@ -94,77 +96,108 @@ class Marketplace extends Resource
'expiration_year' => $expiration_year
));
}
/**
* Create a bank account. These can later be associated with an account
* using \Balanced\Account->addBankAccount.
*
*
* @param string name Name of the account holder.
* @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
*/
public function createBankAccount(
$name,
$account_number,
$bank_code
$routing_number,
$type,
$meta = null
)
{
return $this->bank_accounts->create(array(
'name' => $name,
'name' => $name,
'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
* adding a funding source (e.g a card) or a merchant using
* \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.
*
* @return \Balanced\Account
*/
public function createAccount($email_address, $meta = null)
public function createAccount($email_address = null, $meta = null)
{
return $this->accounts->create(array(
'email_address' => $email_address,
'meta' => $meta,
));
}
/**
* Create a buyer account.
*
* @param string email_address 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.
*
* @return \Balanced\Account
*/
public function createBuyer($email_address, $card_uri, $meta = null)
{
return $this->accounts->create(array(
'email_address' => $email_address,
'card_uri' => $card_uri,
'meta' => $meta,
));
}
/**
* 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
* using \Balanced\Account->promoteToMerchant.
*
* @param string email_address Email address. There can only be one account with this email address.
*
* @return \Balanced\Account
*/
function findOrCreateAccountByEmailAddress($email_address)
{
$marketplace = Marketplace::mine();
try {
$account = $this->accounts->create(array(
'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.
*
*
* Unlike buyers the identity of a merchant must be established before
* 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
* represented as an associative array and passed as the merchant parameter
* when creating the merchant account.
*
*
* For a person the array looks like this:
*
*
* <code>
* array(
* 'type' => 'person',
@ -177,9 +210,9 @@ class Marketplace extends Resource
* 'country_code' => 'USA'
* )
* </code>
*
*
* For a business the array looks like this:
*
*
* <code>
* array(
* 'type' => 'business',
@ -200,10 +233,10 @@ class Marketplace extends Resource
* )
* )
* </code>
*
*
* In some cases the identity of the merchant, person or business, cannot
* be verified in which case a \Balanced\Exceptions\HTTPError is thrown:
*
*
* <code>
* $identity = array(
* 'type' => 'business',
@ -223,7 +256,7 @@ class Marketplace extends Resource
* 'country_code' => 'USA',
* ),
* );
*
*
* try {
* $merchant = \Balanced\Marketplace::mine()->createMerchant(
* 'merchant@example.com',
@ -231,15 +264,15 @@ class Marketplace extends Resource
* );
* catch (\Balanced\Exceptions\HTTPError $e) {
* if ($e->code != 300) {
* throw $e;
* throw $e;
* }
* print e->response->header['Location'] // this is where merchant must signup
* }
* </code>
*
*
* Once the merchant has completed signup you can use the resulting URI to
* create an account for them on your marketplace:
*
*
* <code>
* $merchant = self::$marketplace->createMerchant(
* 'merchant@example.com',
@ -248,24 +281,23 @@ class Marketplace extends Resource
* $merchant_uri
* );
* </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 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 $name Optional name of the merchant.
* @param array[string]string meta Optional metadata to associate with the account.
*
*
* @return \Balanced\Account
*/
public function createMerchant(
$email_address,
$email_address = null,
$merchant = null,
$bank_account_uri = null,
$merchant_uri = null,
$name = null,
$meta = null
)
$meta = null)
{
return $this->accounts->create(array(
'email_address' => $email_address,
@ -276,4 +308,18 @@ class Marketplace extends Resource
'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;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
use \RESTful\URISpec;
/**
* 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
* 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.
*
* @see \Balanced\Marketplace
@ -41,7 +41,7 @@ class Merchant extends Resource
* assert($merchant->id == $owner_account->merchant->id);
* </code>
*
* @throws \Balanced\Exceptions\NoResult
* @throws \RESTful\Exceptions\NoResultFound
* @return \Balanced\Merchant
*/
public static function me()

View File

@ -2,11 +2,11 @@
namespace Balanced;
use Balanced\Core\Resource;
use Balanced\Core\URISpec;
use Balanced\Resource;
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.
*
@ -35,16 +35,16 @@ use Balanced\Core\URISpec;
* 'my_id': '123123'
* )
* );
* </code>
*/
class Refund extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('refunds', 'id');
self::$_registry->add(get_called_class());
}
}
* </code>
*/
class Refund extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('refunds', 'id');
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;
/**
/**
* Configurable settings.
*
*
* You can either set these settings individually:
*
*
* <code>
* \Balanced\Settngs::api_key = 'my-api-key-secret';
* </code>
*
*
* or all at once:
*
*
* <code>
* \Balanced\Settngs::configure(
* 'https://api.balancedpayments.com',
* 'my-api-key-secret'
* );
* </code>
* </code>
*/
class Settings
{
const VERSION = '0.6.6';
const VERSION = '0.7.1';
public static $url_root = 'https://api.balancedpayments.com',
$api_key = null;
$api_key = null,
$agent = 'balanced-php',
$version = Settings::VERSION;
/**
* Configure all settings.
*
*
* @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.
*/
public static function configure($url_root, $api_key)
{
self::$url_root= $url_root;
self::$api_key = $api_key;
}
* @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)
{
self::$url_root= $url_root;
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
namespace Balanced\Core;
namespace RESTful;
class Collection extends Itemization
{
@ -9,40 +9,41 @@ class Collection extends Itemization
parent::__construct($resource, $uri, $data);
$this->_parseUri();
}
private function _parseUri()
{
$parsed = parse_url($this->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]);
// size
if ($key == 'limit') {
$this->_size = $val;
}
}
}
private function _parseUri()
{
$parsed = parse_url($this->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]);
// size
if ($key == 'limit') {
$this->_size = $val;
}
}
}
}
public function create($payload)
{
$class = $this->resource;
$client = $class::getClient();
$response = $client->post($this->uri, $payload);
return new $this->resource($response->body);
}
public function query()
{
return new Query($this->resource, $this->uri);
}
public function paginate()
{
return new Pagination($this->resource, $this->uri);
public function paginate()
{
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
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
* members of the same name.
*
* members of the same name.
*
* @see \Httpful\Response
*/
class HTTPError extends Base
{
public $response;
public function __construct($response)
{
$this->response = $response;
$this->_objectify($this->response->body);
}
protected function _objectify($fields)
{
foreach ($fields as $key => $val)
$this->$key = $val;
protected function _objectify($fields)
{
foreach ($fields as $key => $val) {
$this->$key = $val;
}
}
}

View File

@ -1,11 +1,11 @@
<?php
namespace Balanced\Exceptions;
namespace RESTful\Exceptions;
/**
* Indicates that a query unexpectedly returned multiple results when at most
* 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
namespace Balanced\Core;
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));
}
}
namespace RESTful;
class Itemization implements \IteratorAggregate, \ArrayAccess
{
public $resource,
$uri;
public $resource,
$uri;
protected $_page,
$_offset = 0,
$_size=25;
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)
$_offset = 0,
$_size = 25;
public function __construct($resource, $uri, $data = null)
{
$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);
return $page->items[$offset - $page->offset];
return $page->items[$offset - $page->offset];
}
public function total()
{
return $this->_getPage()->total;
public function total()
{
return $this->_getPage()->total;
}
protected function _buildUri($offset = null)
{
# TODO: hacky but works for now
$offset = ($offset == null) ? $this->_offset : $offset;
if (strpos($this->uri, '?') === false)
$uri = $this->uri . '?';
else
$uri = $this->uri . '&';
$uri = $uri . 'offset=' . strval($offset);
$offset = ($offset == null) ? $this->_offset : $offset;
if (strpos($this->uri, '?') === false) {
$uri = $this->uri . '?';
} else {
$uri = $this->uri . '&';
}
$uri = $uri . 'offset=' . strval($offset);
return $uri;
}
// IteratorAggregate
public function getIterator()
{
$uri = $this->_buildUri($offset = 0);
$uri = $this->_buildUri($offset = 0);
return new ItemizationIterator($this->resource, $uri);
$uri = $this->_buildUri($offset = 0);
return new ItemizationIterator($this->resource, $uri);
}
// ArrayAccess
public function offsetSet($offset, $value)
public function offsetSet($offset, $value)
{
throw new BadMethodCallException(get_class($this) . ' array access is read-only');
}
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');
}
public function offsetExists($offset)
{
throw new BadMethodCallException(get_class($this) . ' array access is read-only');
}
public function offsetGet($offset)
return (0 <= $offset && $offset < $this->total());
}
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
namespace Balanced\Core;
class Page
{
namespace RESTful;
class Page
{
public $resource,
$total,
$items,
$offset,
$limit;
private $_first_uri,
$_previous_uri,
$_next_uri,
$_last_uri;
public function __construct($resource, $uri, $data = null)
$total,
$items,
$offset,
$limit;
private $_first_uri,
$_previous_uri,
$_next_uri,
$_last_uri;
public function __construct($resource, $uri, $data = null)
{
$this->resource = $resource;
if ($data == null) {
$client = $resource::getClient();
$data = $client->get($uri)->body;
$this->resource = $resource;
if ($data == null) {
$client = $resource::getClient();
$data = $client->get($uri)->body;
}
$this->total = $data->total;
$this->total = $data->total;
$this->items = array_map(
function($x) use ($resource) {
function ($x) use ($resource) {
return new $resource($x);
},
$data->items);
$this->offset = $data->offset;
$this->limit = $data->limit;
$this->_first_uri = $data->first_uri;
$this->_previous_uri = $data->previous_uri;
$this->_next_uri = $data->next_uri;
$this->_last_uri = $data->last_uri;
}
public function first()
{
return new Page($this->resource, $this->_first_uri);
}
public function next()
{
if (!$this->hasNext())
return null;
return new Page($this->resource, $this->_next_uri);
}
public function hasNext()
{
return $this->_next_uri != null;
}
public function previous()
{
return new Page($this->resource, $this->_previous_uri);
}
public function hasPrevious()
{
return $this->_previous_uri != null;
}
public function last()
{
return new Page($this->resource, $this->_last_uri);
}
}
$this->limit = $data->limit;
$this->_first_uri = property_exists($data, 'first_uri') ? $data->first_uri : null;
$this->_previous_uri = property_exists($data, 'previous_uri') ? $data->previous_uri : null;
$this->_next_uri = property_exists($data, 'next_uri') ? $data->next_uri : null;
$this->_last_uri = property_exists($data, 'last_uri') ? $data->last_uri : null;
}
public function first()
{
return new Page($this->resource, $this->_first_uri);
}
public function next()
{
if (!$this->hasNext()) {
return null;
}
return new Page($this->resource, $this->_next_uri);
}
public function hasNext()
{
return $this->_next_uri != null;
}
public function previous()
{
return new Page($this->resource, $this->_previous_uri);
}
public function hasPrevious()
{
return $this->_previous_uri != null;
}
public function last()
{
return new Page($this->resource, $this->_last_uri);
}
}

View File

@ -1,125 +1,90 @@
<?php
namespace Balanced\Core;
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;
}
}
class Pagination implements \IteratorAggregate, \ArrayAccess
class Pagination implements \IteratorAggregate, \ArrayAccess
{
public $resource,
$uri;
$uri;
protected $_page,
$_offset=0,
$_size=25;
public function __construct($resource, $uri, $data = null)
$_offset = 0,
$_size = 25;
public function __construct($resource, $uri, $data = null)
{
$this->resource = $resource;
$this->uri = $uri;
if ($data != null)
if ($data != null) {
$this->_page = new Page($resource, $uri, $data);
else
$this->_page = null;
} else {
$this->_page = null;
}
}
protected function _getPage($offset = null)
{
if ($this->_page == null) {
$this->_offset = ($offset == null) ? 0 : $offset * $this->_size;
$uri = $this->_buildUri();
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
}
else if ($offset != null) {
} elseif ($offset != null) {
$offset = $offset * $this->_size;
if ($offset != $this->_offset) {
$this->_offset = $offset;
$uri = $this->_buildUri();
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
}
}
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)
{
# TODO: hacky but works for now
$offset = ($offset == null) ? $this->_offset : $offset;
if (strpos($this->uri, '?') === false)
if (strpos($this->uri, '?') === false) {
$uri = $this->uri . '?';
else
} else {
$uri = $this->uri . '&';
}
$uri = $uri . 'offset=' . strval($offset);
return $uri;
}
// IteratorAggregate
public function getIterator()
public function getIterator()
{
$uri = $this->_buildUri($offset = 0);
return new PaginationIterator($this->resource, $uri);
$uri = $this->_buildUri($offset = 0);
return new PaginationIterator($this->resource, $uri);
}
// ArrayAccess
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)
{
return (0 <= $offset && $offset < $this->total());
return (0 <= $offset && $offset < $this->total());
}
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)
{
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
namespace Balanced\Core;
namespace RESTful;
use Balanced\Exceptions\NoResultFound;
use Balanced\Exceptions\MultipleResultsFound;
use RESTful\Exceptions\NoResultFound;
use RESTful\Exceptions\MultipleResultsFound;
class Query extends Itemization
{
class Query extends Itemization
{
public $filters = array(),
$sorts = array(),
$size;
public function __construct($resource, $uri)
{
$sorts = array(),
$size;
public function __construct($resource, $uri)
{
parent::__construct($resource, $uri);
$this->size = $this->_size;
$this->_parseUri($uri);
$this->size = $this->_size;
$this->_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)
private function _parseUri($uri)
{
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);
$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);
}
}
}
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());
}
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 == '=') {
$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
namespace Balanced\Core;
namespace RESTful;
class Registry
{
protected $_resources = array();
public function add($resource) {
public function add($resource)
{
array_push($this->_resources, $resource);
}
public function match($uri) {
public function match($uri)
{
foreach ($this->_resources as $resource) {
$spec = $resource::getURISpec();
$result = $spec->match($uri);
if ($result == null)
continue;
if ($result == null) {
continue;
}
$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
namespace Balanced\Core;
namespace RESTful;
class URISpec
{
public $collection_uri = null,
$name,
$idNames;
$name,
$idNames;
public function __construct($name, $idNames, $root = null)
{
$this->name = $name;
if (!is_array($idNames))
if (!is_array($idNames)) {
$idNames = array($idNames);
}
$this->idNames = $idNames;
if ($root != null)
$this->collection_uri = $root . '/' . $name;
if ($root != null) {
if ($root == '' || substr($root, -1) == '/') {
$this->collection_uri = $root . $name;
} else {
$this->collection_uri = $root . '/' . $name;
}
}
}
public function match($uri)
{
$parts = explode('/', $uri);
$parts = explode('/', rtrim($uri, "/"));
// collection
if ($parts[count($parts) - 1] == $this->name)
if ($parts[count($parts) - 1] == $this->name) {
return array(
'collection' => true,
);
);
}
// non-member
if (count($parts) < count($this->idNames) + 1 ||
$parts[count($parts) - 1 - count($this->idNames)] != $this->name)
if (count($parts) < count($this->idNames) + 1 ||
$parts[count($parts) - 1 - count($this->idNames)] != $this->name
) {
return null;
}
// member
$ids = array_combine(
$this->idNames,
array_slice($parts, -count($this->idNames))
);
$result = array(
);
$result = array(
'collection' => false,
'ids' => $ids,
);
'ids' => $ids,
);
return $result;
}
}