blackoptic/xerobundle
Symfony bundle that wraps the Xero API with a Guzzle-based client. Configure your Xero consumer key/secret and private key, then fetch resources like Invoices via the blackoptic.xero.client service for simple authenticated requests.
Installation Add the package via Composer:
composer require blackoptic/xerobundle
Ensure your composer.json includes the package under require.
Bundle Registration
Register the bundle in config/bundles.php (Symfony 4+) or app/AppKernel.php (Symfony 2/3):
BlackOptic\Bundle\XeroBundle\BlackOpticXeroBundle::class => ['all' => true],
Configuration
Add Xero API credentials to config/packages/black_optic_xero.yaml (Symfony 4+) or app/config/config.yml (Symfony 2/3):
black_optic_xero:
consumer_key: "%env(XERO_CONSUMER_KEY)%"
consumer_secret: "%env(XERO_CONSUMER_SECRET)%"
private_key: "%kernel.project_dir%/config/xero_private_key.p8"
Store sensitive keys in .env:
XERO_CONSUMER_KEY=your_consumer_key
XERO_CONSUMER_SECRET=your_consumer_secret
First API Call Inject the Xero client service into a controller or command:
use BlackOptic\Bundle\XeroBundle\Service\XeroClient;
class InvoiceController extends AbstractController
{
public function __construct(private XeroClient $xeroClient) {}
public function listInvoices()
{
$response = $this->xeroClient->get('Invoices')->send();
return $this->json($response->json());
}
}
OAuth2 Authentication The bundle handles OAuth2 token generation automatically. Refresh tokens silently if expired:
$response = $this->xeroClient->get('Contacts')->send();
Resource Operations Use standard HTTP methods for CRUD operations:
// Create
$this->xeroClient->post('Invoices', ['Invoice' => $invoiceData])->send();
// Update
$this->xeroClient->put("Invoices/{$invoiceId}", ['Invoice' => $updatedData])->send();
// Delete
$this->xeroClient->delete("Invoices/{$invoiceId}")->send();
Pagination
Handle paginated responses with XeroPaginator:
$paginator = new XeroPaginator($this->xeroClient, 'Contacts');
foreach ($paginator as $page) {
foreach ($page as $contact) {
// Process contact
}
}
Webhooks
Register webhook endpoints in config/packages/black_optic_xero.yaml:
black_optic_xero:
webhooks:
- endpoint: /xero/webhook
events: ['CREATED', 'UPDATED']
Error Handling Use middleware to catch Xero API errors:
$this->xeroClient->get('Invoices')->onResponse(function (Response $response) {
if ($response->getStatusCode() === 400) {
throw new \RuntimeException($response->json()['error']['message']);
}
})->send();
Dependency Injection
Prefer constructor injection for the XeroClient service to ensure testability:
public function __construct(private XeroClient $xeroClient) {}
Configuration Overrides
Override default config in config/packages/black_optic_xero.yaml:
black_optic_xero:
base_uri: 'https://api.xero.com/api.xro/2.0' # Override if needed
timeout: 30
Logging Enable Guzzle logging for debugging:
black_optic_xero:
options:
on_request: [BlackOptic\Bundle\XeroBundle\Logger\XeroRequestLogger]
Testing
Mock the XeroClient in tests:
$mockClient = $this->createMock(XeroClient::class);
$mockClient->method('get')->willReturn(new Response(200, [], json_encode(['Invoices' => []])));
$this->container->set('blackoptic.xero.client', $mockClient);
Batch Processing Use Xero’s batch endpoints for bulk operations:
$batch = $this->xeroClient->batch();
$batch->add('POST', 'Invoices', ['Invoice' => $data1]);
$batch->add('POST', 'Invoices', ['Invoice' => $data2]);
$batch->send();
Private Key Path
%kernel.project_dir%:
private_key: "%env(XERO_PRIVATE_KEY_PATH)%"
Token Expiry
401 Unauthorized errors.$this->xeroClient->setOption('on_request', function (RequestInterface $request) {
if ($request->hasHeader('Authorization') && strpos($request->getHeader('Authorization')[0], 'Bearer') === 0) {
// Handle token refresh logic here
}
});
Rate Limiting
$this->xeroClient->get('Invoices')->delay(100)->send();
Webhook Verification
X-Xero-Request-ID header.public function handleWebhook(Request $request)
{
$xeroRequestId = $request->headers->get('X-Xero-Request-ID');
if (!$xeroRequestId) {
throw new \RuntimeException('Invalid Xero webhook request');
}
// Process payload
}
Deprecated Endpoints
api.xero.com → api.xero.com/api.xro/2.0).base_uri in config.Enable Guzzle Debugging Add this to your config to log all requests/responses:
black_optic_xero:
options:
debug: true
on_request: [GuzzleHttp\Middleware::log]
on_response: [GuzzleHttp\Middleware::log]
Check Token Validity Verify your OAuth2 token hasn’t expired:
$token = $this->xeroClient->getToken();
if ($token->getExpiresAt() < new \DateTime()) {
$this->xeroClient->refreshToken();
}
Validate Private Key
Ensure your .p8 private key is correctly formatted (PEM format):
openssl rsa -in xero_private_key.p8 -inform PEM -pubout
If invalid, regenerate the key in the Xero Developer Portal.
Handle Idempotency
Use Xero’s Idempotency-Key header for safe retries:
$this->xeroClient->post('Invoices', ['Invoice' => $data])
->setHeader('Idempotency-Key', uniqid())
->send();
Custom Middleware Add middleware to modify requests/responses:
$this->xeroClient->getMiddleware()->push(
\GuzzleHttp\Middleware::mapRequest(function (RequestInterface $request) {
$request = $request->withHeader('X-Custom-Header', 'value');
return $request;
})
);
Event Listeners Subscribe to Xero events (e.g., after token refresh):
// In a service
public function __construct(private XeroClient $xeroClient)
{
$this->xeroClient->getEventDispatcher()->addListener(
XeroEvents::TOKEN_REFRESHED,
function (TokenRefreshedEvent $event) {
// Log or notify about token refresh
}
);
}
Custom Response Handlers Override response handling for
How can I help you explore Laravel packages today?