-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allows for uploads of images and attachments. Includes documentation on invoice usage. Expense usage documentation to follow. Issue#67
- Loading branch information
Showing
10 changed files
with
454 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
File Uploads | ||
============ | ||
|
||
Some FreshBooks resource can include images and attachments. For example, invoices can have a company | ||
logo or banner image as part of the invoice presentation object as well as images or pdfs attachments. | ||
Expenses can also include copies or photos of receipts as attachments. | ||
|
||
All images and attachments first need to be uploaded to FreshBooks via the ``images`` or ``attachments`` | ||
endpoints. | ||
|
||
These will then return a path to your file with a JWT. This path will can then be passed as part of the | ||
data in a subsequent call. | ||
|
||
See FreshBooks' `invoice attachment <https://www.freshbooks.com/api/invoice_presentation_attachments>`_ | ||
and `expense attachment <https://www.freshbooks.com/api/https://www.freshbooks.com/api/expense-attachments>`_ | ||
documentation for more information. | ||
|
||
Invoice Images and Attachments | ||
------------------------------ | ||
|
||
See `FreshBooks' API Documentation <https://www.freshbooks.com/api/invoice_presentation_attachments>`_. | ||
|
||
The ``upload()`` function takes a `PHP resource <https://www.php.net/manual/en/language.types.resource.php>`_. | ||
Logo's and banners are added to the invoice presentation objec.t To include an uploaded attachment on | ||
an invoice, the invoice request must include an attachments object. | ||
|
||
.. code-block:: php | ||
$logo = $freshBooksClient->images()->upload($accountId, fopen('./sample_logo.png', 'r')); | ||
$attachment = $freshBooksClient->attachments()->upload($accountId, fopen('./sample_attachment.pdf', 'r')); | ||
$presentation = [ | ||
'image_logo_src' => "/uploads/images/{$logo->jwt}", | ||
'theme_primary_color' => '#1fab13', | ||
'theme_layout' => 'simple' | ||
]; | ||
$invoiceData = [ | ||
'customerid' => $clientId, | ||
'attachments' => [ | ||
[ | ||
'jwt' => $attachment->jwt, | ||
'media_type' => $attachment->mediaType | ||
] | ||
], | ||
'presentation' => presentation | ||
]; | ||
$invoice = $freshBooksClient->invoices()->create($accountId, $invoiceData); | ||
Expense Receipts | ||
---------------- | ||
|
||
See `FreshBooks' API Documentation <https://www.freshbooks.com/api/expense-attachments>`_. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
configuration | ||
authorization | ||
api-calls/index | ||
file-uploads | ||
webhooks | ||
examples | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace amcintosh\FreshBooks\Model; | ||
|
||
use Psr\Http\Message\StreamInterface; | ||
|
||
/** | ||
* A file that has been uploaded to FreshBooks. | ||
* | ||
* @package amcintosh\FreshBooks\Model | ||
*/ | ||
class FileUpload | ||
{ | ||
/** | ||
* @var string The JWT used to fetch the file from FreshBooks. | ||
*/ | ||
public ?string $jwt; | ||
|
||
/** | ||
* @var string The name of the file uploaded to FreshBooks. | ||
* | ||
* This is returned from the API in the `X-filename` header. | ||
*/ | ||
public ?string $fileName; | ||
|
||
/** | ||
* @var string The media type (eg. `image/png`) of the file uploaded to FreshBooks. | ||
*/ | ||
public ?string $mediaType; | ||
|
||
/** | ||
* @var string The PSR StreamInterface steam of data from the request body. | ||
*/ | ||
public ?StreamInterface $responseBody; | ||
|
||
/** | ||
* @var string A fully qualified path the the file from FreshBooks. | ||
*/ | ||
public ?string $link; | ||
|
||
public function __construct(?string $fileName, ?string $mediaType, ?StreamInterface $responseBody) | ||
{ | ||
$this->fileName = $fileName; | ||
$this->mediaType = $mediaType; | ||
$this->responseBody = $responseBody; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace amcintosh\FreshBooks\Resource; | ||
|
||
use Http\Client\HttpClient; | ||
use Http\Discovery\HttpClientDiscovery; | ||
use Http\Discovery\Psr17FactoryDiscovery; | ||
use Http\Message\MultipartStream\MultipartStreamBuilder; | ||
use amcintosh\FreshBooks\Exception\FreshBooksException; | ||
use amcintosh\FreshBooks\Model\FileUpload; | ||
|
||
class UploadResource extends BaseResource | ||
{ | ||
protected HttpClient $httpClient; | ||
protected string $uploadPath; | ||
protected string $resourceName; | ||
|
||
public function __construct( | ||
HttpClient $httpClient, | ||
string $uploadPath, | ||
string $resourceName, | ||
) { | ||
$this->httpClient = $httpClient; | ||
$this->uploadPath = $uploadPath; | ||
$this->resourceName = $resourceName; | ||
} | ||
|
||
/** | ||
* The the url to the upload resource. | ||
* | ||
* @param string $accountId | ||
* @param int $resourceId | ||
* @return string | ||
*/ | ||
protected function getUrl(string $accountId = null, string $jwt = null): string | ||
{ | ||
if (!is_null($accountId)) { | ||
return "/uploads/account/{$accountId}/{$this->uploadPath}"; | ||
} | ||
return "/uploads/{$this->uploadPath}/{$jwt}"; | ||
} | ||
|
||
/** | ||
* Create a FreshBooksException from the json response from the uploads endpoint. | ||
* | ||
* @param int $statusCode HTTP status code | ||
* @param string $contents The response contents | ||
* @return void | ||
*/ | ||
protected function handleError(int $statusCode, string $contents): void | ||
{ | ||
try { | ||
$responseData = json_decode($contents, true); | ||
} catch (JSONDecodeError $e) { | ||
throw new FreshBooksException('Failed to parse response', $statusCode, $e, $contents); | ||
} | ||
$message = $responseData['error'] ?? "Unknown error"; | ||
throw new FreshBooksException($message, $statusCode, null, $contents, null, null); | ||
} | ||
|
||
/** | ||
* Make a request against the uploads resource. Returns an object containing a | ||
* Psr\Http\Message\StreamInterface for flexibility. | ||
* Throws a FreshBooksException if the response is not a 200. | ||
* | ||
* @param string $url | ||
* @return FileUpload | ||
*/ | ||
private function makeGetFileRequest(string $url): FileUpload | ||
{ | ||
$response = $this->httpClient->send(self::GET, $url); | ||
$responseBody = $response->getBody(); | ||
$statusCode = $response->getStatusCode(); | ||
if ($statusCode >= 400) { | ||
$this->handleError($statusCode, $responseBody->getContents()); | ||
} | ||
$fileName = $response->getHeader('X-filename'); | ||
if (!is_null($fileName) && count($fileName) > 0) { | ||
$fileName = $fileName[0]; | ||
} | ||
$mediaType = $response->getHeader('Content-Type'); | ||
if (!is_null($mediaType) && count($mediaType) > 0) { | ||
$mediaType = $mediaType[0]; | ||
} | ||
|
||
return new FileUpload($fileName, $mediaType, $responseBody); | ||
} | ||
|
||
/** | ||
* Make creates a POST request to upload a file to FreshBooks. | ||
* Throws a FreshBooksException if the response is not a 200. | ||
* | ||
* @param string $url | ||
* @return FileUpload | ||
*/ | ||
private function makeUploadRequest(string $url, $file): FileUpload | ||
{ | ||
// Thank you https://dev.to/timoschinkel/sending-multipart-data-with-psr-18-2lb5 | ||
$streamFactory = Psr17FactoryDiscovery::findStreamFactory(); | ||
|
||
$builder = new MultipartStreamBuilder($streamFactory); | ||
$builder->addResource('content', $file); | ||
|
||
$requestFactory = Psr17FactoryDiscovery::findRequestFactory(); | ||
$request = $requestFactory | ||
->createRequest('POST', $url) | ||
->withHeader('Content-Type', 'multipart/form-data; boundary="' . $builder->getBoundary() . '"') | ||
->withBody($builder->build()); | ||
|
||
$response = $this->httpClient->sendRequest($request); | ||
|
||
$statusCode = $response->getStatusCode(); | ||
$responseBody = $response->getBody(); | ||
if ($statusCode >= 400) { | ||
$this->handleError($statusCode, $responseBody->getContents()); | ||
} | ||
try { | ||
$contents = $responseBody->getContents(); | ||
$responseData = json_decode($contents, true); | ||
} catch (JSONDecodeError $e) { | ||
throw new FreshBooksException('Failed to parse response', $statusCode, $e, $contents); | ||
} | ||
if (is_null($responseData) || !array_key_exists($this->resourceName, $responseData)) { | ||
throw new FreshBooksException('Returned an unexpected response', $statusCode, null, $contents); | ||
} | ||
$link = $responseData['link'] ?? null; | ||
$responseData = $responseData[$this->resourceName]; | ||
$fileData = new FileUpload($responseData['filename'], $responseData['media_type'], null); | ||
$fileData->link = $link; | ||
$fileData->jwt = $responseData['jwt']; | ||
return $fileData; | ||
} | ||
|
||
/** | ||
* Get an uploaded file. | ||
* | ||
* @param string $jwt JWT provided by FreshBooks when the file was uploaded. | ||
* @return FileUpload Object containing the file name, content type, and stream of data. | ||
*/ | ||
public function get(string $jwt): FileUpload | ||
{ | ||
return $this->makeGetFileRequest($this->getUrl(jwt: $jwt)); | ||
} | ||
|
||
/** | ||
* Upload a file to FreshBooks. | ||
* | ||
* @param string $accountId The alpha-numeric account id | ||
* @param resource File resource to upload | ||
* @return FileUpload Object containing the JWT, file name, content type. | ||
*/ | ||
public function upload(string $accountId, $file): FileUpload | ||
{ | ||
$url = $this->getUrl($accountId); | ||
return $this->makeUploadRequest($url, $file); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.