diff --git a/src/XeroPHP/Application.php b/src/XeroPHP/Application.php index b6c19e60..bd0891a1 100644 --- a/src/XeroPHP/Application.php +++ b/src/XeroPHP/Application.php @@ -121,7 +121,7 @@ public function loadByGUID($model, $guid) { //Return the first (if any) element from the response. foreach($request->getResponse()->getElements() as $element){ - $object = new $class(); + $object = new $class($this); $object->fromStringArray($element); return $object; } @@ -156,13 +156,14 @@ public function save(Object $object) { //In this case it's new if($object->hasGUID()) { - $method = Request::METHOD_POST; $uri = sprintf('%s/%s', $object::getResourceURI(), $object->getGUID()); } else { $method = Request::METHOD_PUT; $uri = $object::getResourceURI(); + //@todo, bump version so you must create objects with app context. + $object->setApplication($this); } if(!$object::supportsMethod($method)) diff --git a/src/XeroPHP/Models/Accounting/Attachment.php b/src/XeroPHP/Models/Accounting/Attachment.php new file mode 100644 index 00000000..55e9b0e2 --- /dev/null +++ b/src/XeroPHP/Models/Accounting/Attachment.php @@ -0,0 +1,204 @@ + array(false, self::PROPERTY_TYPE_STRING, null, false, false), + 'FileName' => array(true, self::PROPERTY_TYPE_STRING, null, false, false), + 'Url' => array(false, self::PROPERTY_TYPE_STRING, null, false, false), + 'MimeType' => array(false, self::PROPERTY_TYPE_STRING, null, false, false), + 'ContentLength' => array(false, self::PROPERTY_TYPE_INT, null, false, false) + ); + } + + /** + * Get a list of the supported HTTP Methods + * + * @return array + */ + static function getSupportedMethods() { + return array( + Request::METHOD_GET, + Request::METHOD_PUT, + Request::METHOD_POST + ); + } + + /** + * return the URI of the resource (if any) + * + * @return string + */ + static function getResourceURI() { + return ''; + } + + + //Do this with a file handle please + public static function createFromLocalFile($file_name, $mime_type = null){ + + //Try to guess. Might be questionable on non-*nix machines + if($mime_type === null){ + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $info = $finfo->file($file_name); + + if($info !== false){ + $mime_type = $info; + } + } + + $content_length = filesize($file_name); + $path_info = pathinfo($file_name); + + $instance = new self(); + $instance->fromStringArray(array( + 'MimeType' => $mime_type, + 'ContentLength' => $content_length, + 'FileName' => $path_info['basename'] + )); + $instance->setLocalHandle(fopen($file_name, 'r')); + + return $instance; + } + + /** + * @return string + */ + public function getContent() { + + if(!isset($this->content)){ + //If it's been created locally, you can just read it back. + if(isset($this->local_handle)){ + rewind($this->local_handle); + while(!feof($this->local_handle)){ + $this->content .= fread($this->local_handle, 8192); + } + //Otherwise, if it can be fetched + } elseif(isset($this->_data['Url'])){ + $this->content = self::downloadContent($this->_application, $this->_data['Url']); + } + } + + return $this->content; + + } + + + private static function downloadContent(Application $app, $url){ + + $url = new URL($app, $url); + $request = new Request($app, $url, Request::METHOD_GET); + $request->setHeader(Request::HEADER_ACCEPT, '*/*'); + + $request->send(); + + return $request->getResponse()->getResponseBody(); + } + + + /** + * @return string + */ + public function getAttachmentID() { + return $this->_data['AttachmentID']; + + } + + /** + * @return int + */ + public function getContentLength() { + return $this->_data['ContentLength']; + } + + /** + * @return string + */ + public function getFileName() { + return $this->_data['FileName']; + } + + /** + * @return string + */ + public function getMimeType() { + return $this->_data['MimeType']; + } + + /** + * @return string + */ + public function getUrl() { + return $this->_data['Url']; + } + + /** + * @return mixed + */ + public function getLocalHandle() { + return $this->local_handle; + } + + /** + * @param mixed $local_handle + */ + public function setLocalHandle($local_handle) { + $this->local_handle = $local_handle; + } + +} \ No newline at end of file diff --git a/src/XeroPHP/Remote/Object.php b/src/XeroPHP/Remote/Object.php index f0826e6a..16c0e73b 100644 --- a/src/XeroPHP/Remote/Object.php +++ b/src/XeroPHP/Remote/Object.php @@ -2,6 +2,7 @@ namespace XeroPHP\Remote; +use XeroPHP\Application; use XeroPHP\Exception; use XeroPHP\Helpers; @@ -32,13 +33,6 @@ abstract class Object implements ObjectInterface { const PROPERTY_TYPE_DATE = 'date'; const PROPERTY_TYPE_OBJECT = 'object'; - - public function __construct() { - $this->_dirty = array(); - $this->_data = array(); - $this->_associated_objects = array(); - } - /** * Container to the actual properties of the object * @@ -61,6 +55,30 @@ public function __construct() { protected $_associated_objects; + /** + * Holds a ref to the application that was used to load the object, enables shorthand $object->save(); + * + * @var Application $_application + */ + protected $_application; + + + public function __construct(Application $application = null) { + $this->_application = $application; + $this->_dirty = array(); + $this->_data = array(); + $this->_associated_objects = array(); + } + + /** + * This should be compulsory in the constructor int he future, but will have to be like this for BC until the next major version. + * + * @param Application $application + */ + public function setApplication(Application $application){ + $this->_application = $application; + } + /** * If there have been any properties changed since load * @@ -228,7 +246,6 @@ public static function castToString($type, $value) { /** * Cast the values to PHP types * - * @param $property_name * @param $type * @param $value * @param $php_type @@ -307,6 +324,18 @@ public function validate($check_children = true) { } + /** + * Shorthand save an object if it is instantiated with app context. + * + * @throws Exception + */ + public function save(){ + if($this->_application === null){ + throw new Exception('->save() is only available on objects that have an injected application context.'); + } + $this->_application->save($this); + } + /** * @param string $property * @param self $object diff --git a/src/XeroPHP/Remote/ObjectInterface.php b/src/XeroPHP/Remote/ObjectInterface.php index c3c9e5f9..9149a8ea 100644 --- a/src/XeroPHP/Remote/ObjectInterface.php +++ b/src/XeroPHP/Remote/ObjectInterface.php @@ -26,4 +26,13 @@ static function getProperties(); * @return array */ static function getSupportedMethods(); + + /** + * return the URI of the resource (if any) + * + * @return string + */ + static function getResourceURI(); + + } \ No newline at end of file diff --git a/src/XeroPHP/Remote/Query.php b/src/XeroPHP/Remote/Query.php index 0c62271e..af6ec4a6 100644 --- a/src/XeroPHP/Remote/Query.php +++ b/src/XeroPHP/Remote/Query.php @@ -107,7 +107,7 @@ public function execute() { $elements = array(); foreach($request->getResponse()->getElements() as $element) { - $built_element = new $from_class; + $built_element = new $from_class($this->app); $built_element->fromStringArray($element); $elements[] = $built_element; } diff --git a/src/XeroPHP/Remote/Request.php b/src/XeroPHP/Remote/Request.php index 72bd031b..ca511d7a 100644 --- a/src/XeroPHP/Remote/Request.php +++ b/src/XeroPHP/Remote/Request.php @@ -34,8 +34,8 @@ class Request { private $oauth_client; - /* - * @var XeroPHP\Remote\Response; + /** + * @var \XeroPHP\Remote\Response; */ private $response; @@ -135,8 +135,8 @@ public function getHeaders() { return $this->headers; } - /* - * @return XeroPHP\Remote\Response + /** + * @return \XeroPHP\Remote\Response */ public function getResponse() { if(isset($this->response)) diff --git a/src/XeroPHP/Remote/Response.php b/src/XeroPHP/Remote/Response.php index 07bc2d2a..7698ac27 100644 --- a/src/XeroPHP/Remote/Response.php +++ b/src/XeroPHP/Remote/Response.php @@ -53,6 +53,17 @@ public function __construct(Request $request, $response_body, array $curl_info) list($this->content_type) = explode(';', $curl_info['content_type']); } + /** + * @throws BadRequestException + * @throws Exception + * @throws InternalErrorException + * @throws NotAvailableException + * @throws NotFoundException + * @throws NotImplementedException + * @throws OrganisationOfflineException + * @throws RateLimitExceededException + * @throws UnauthorizedException + */ public function parse() { $this->parseBody(); @@ -67,6 +78,7 @@ public function parse() { throw new BadRequestException(); } + /** @noinspection PhpMissingBreakStatementInspection */ case Response::STATUS_UNAUTHORISED: //This is where OAuth errors end up, this could maybe change to an OAuth exception if(isset($this->oauth_response['oauth_problem_advice'])) { @@ -98,6 +110,10 @@ public function parse() { } } + public function getResponseBody(){ + return $this->response_body; + } + public function getElements() { return $this->elements; } @@ -128,8 +144,10 @@ public function getOAuthResponse() { public function parseBody() { - if($this->request->getUrl()->isOAuth()) - return $this->parseHTML(); + if($this->request->getUrl()->isOAuth()){ + $this->parseHTML(); + return; + } $this->elements = array(); $this->element_errors = array(); @@ -150,7 +168,8 @@ public function parseBody() { break; default: - throw new Exception("Parsing method not implemented for [{$this->content_type}]"); + //Don't try to parse anything else. + return; } foreach($this->elements as $index => $element) @@ -186,10 +205,10 @@ public function findElementErrors($element, $element_index) { public function parseXML() { $sxml = new SimpleXMLElement($this->response_body); - $root_error = array(); // For lack of a better way to find the elements returned (every time) // XML has an array 2 levels deep due to its non-unique key nature. + /** @var SimpleXMLElement $root_child */ foreach($sxml as $child_index => $root_child) { switch($child_index) { @@ -214,7 +233,6 @@ public function parseXML() { public function parseJSON() { $json = json_decode($this->response_body, true); - $root_error = array(); foreach($json as $child_index => $root_child) { diff --git a/src/XeroPHP/Remote/URL.php b/src/XeroPHP/Remote/URL.php index 6a7d70b3..9aff5a1e 100644 --- a/src/XeroPHP/Remote/URL.php +++ b/src/XeroPHP/Remote/URL.php @@ -43,6 +43,16 @@ class URL { */ public function __construct(Application $app, $endpoint, $api = null) { + //Handle full URLs and pull them back apart. + //Annoying internal references are http??? and absolute. + if(strpos($endpoint, 'http') === 0){ + if(preg_match('@^http(s)?://[^/]+/(?[^/]+)/(?[^/]+)/(?.+)$@i', $endpoint, $matches)) { + $endpoint = $matches['endpoint']; + $api = $matches['api']; + //$version = $matches['version']; + } + } + if($api === null) { //Assume that it's an OAuth endpoint if no API is given. //If this becomes an issue it can just check every time, but it seems a little redundant diff --git a/src/XeroPHP/Traits/AttachmentTrait.php b/src/XeroPHP/Traits/AttachmentTrait.php new file mode 100644 index 00000000..62d1e56b --- /dev/null +++ b/src/XeroPHP/Traits/AttachmentTrait.php @@ -0,0 +1,60 @@ +getGUID(), $attachment->getFileName()); + + $url = new URL($this->_application, $uri); + $request = new Request($this->_application, $url, Request::METHOD_POST); + print_r($request); + $request->send(); + + $request->setBody($attachment->getContent())->send(); + $response = $request->getResponse(); + + if(false !== $element = current($response->getElements())) { + $attachment->fromStringArray($element); + } + + return $this; + + } + + public function getAttachments(){ + + /** @var Object $this */ + + if($this->hasGUID() === false){ + throw new Exception('Attachments are only available to objects that exist remotely.'); + } + + $uri = sprintf('%s/%s/Attachments', $this::getResourceURI(), $this->getGUID()); + + $url = new URL($this->_application, $uri); + $request = new Request($this->_application, $url, Request::METHOD_GET); + $request->send(); + + $attachments = array(); + foreach($request->getResponse()->getElements() as $element) { + $attachment = new Attachment($this->_application); + $attachment->fromStringArray($element); + $attachments[] = $attachment; + } + + return $attachments; + + + } + +} \ No newline at end of file diff --git a/src/XeroPHP/Traits/PDFTrait.php b/src/XeroPHP/Traits/PDFTrait.php new file mode 100644 index 00000000..9cb862ff --- /dev/null +++ b/src/XeroPHP/Traits/PDFTrait.php @@ -0,0 +1,31 @@ +hasGUID() === false){ + throw new Exception('PDF files are only available to objects that exist remotely.'); + } + + $uri = sprintf('%s/%s', $this::getResourceURI(), $this->getGUID()); + + $url = new URL($this->_application, $uri); + $request = new Request($this->_application, $url, Request::METHOD_GET); + $request->setHeader(Request::HEADER_ACCEPT, Request::CONTENT_TYPE_PDF); + + $request->send(); + + return $request->getResponse()->getResponseBody(); + + } + +} \ No newline at end of file