{"id":20629922,"url":"https://github.com/academe/xerophp","last_synced_at":"2026-04-27T05:05:13.189Z","repository":{"id":56939861,"uuid":"108985931","full_name":"academe/XeroPHP","owner":"academe","description":"PHP library for working with the Xero OAuth API.","archived":false,"fork":false,"pushed_at":"2017-12-11T15:36:16.000Z","size":276,"stargazers_count":0,"open_issues_count":4,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-17T07:07:12.392Z","etag":null,"topics":["api","guzzle","json","oauth","rest-api","xero","xero-api","xero-provider"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/academe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-31T11:26:49.000Z","updated_at":"2017-11-12T11:32:11.000Z","dependencies_parsed_at":"2022-08-21T01:40:22.719Z","dependency_job_id":null,"html_url":"https://github.com/academe/XeroPHP","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/academe%2FXeroPHP","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/academe%2FXeroPHP/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/academe%2FXeroPHP/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/academe%2FXeroPHP/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/academe","download_url":"https://codeload.github.com/academe/XeroPHP/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242583290,"owners_count":20153421,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","guzzle","json","oauth","rest-api","xero","xero-api","xero-provider"],"created_at":"2024-11-16T14:06:26.047Z","updated_at":"2026-04-27T05:05:13.139Z","avatar_url":"https://github.com/academe.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/academe/XeroPHP.svg?branch=master)](https://travis-ci.org/academe/XeroPHP)\n[![Latest Stable Version](https://poser.pugx.org/academe/xero-php/v/stable)](https://packagist.org/packages/academe/xero-php)\n[![Total Downloads](https://poser.pugx.org/academe/xero-php/downloads)](https://packagist.org/packages/academe/xero-php)\n[![Latest Unstable Version](https://poser.pugx.org/academe/xero-php/v/unstable)](https://packagist.org/packages/academe/xero-php)\n[![License](https://poser.pugx.org/academe/xero-php/license)](https://packagist.org/packages/academe/xero-php)\n\nTable of Contents\n=================\n\n   * [XeroPHP API OAuth Access](#xerophp-api-oauth-access)\n      * [Intro](#intro)\n      * [Areas to Complete (TODO)](#areas-to-complete-todo)\n      * [Quick Start](#quick-start)\n      * [The Response Message](#the-response-message)\n         * [Message Instantiation](#message-instantiation)\n         * [Response Collections](#response-collections)\n      * [Guzzle Exceptions](#guzzle-exceptions)\n      * [Catching Errors](#catching-errors)\n      * [API Response Structures](#api-response-structures)\n         * [A: Single metadata header; single resource](#a-single-metadata-header-single-resource)\n         * [B: Single metadata header; collection of resources](#b-single-metadata-header-collection-of-resources)\n         * [C: Single metadata header; collection of a single resource](#c-single-metadata-header-collection-of-a-single-resource)\n         * [D: Single metadata header; collection of resources](#d-single-metadata-header-collection-of-resources)\n         * [E: Array of resources](#e-array-of-resources)\n         * [F: Single resources](#f-single-resources)\n      * [Other Notes](#other-notes)\n\nXeroPHP API OAuth Access\n========================\n\nPHP library for working with the Xero OAuth API.\n\nIntro\n-----\n\nThis library tackles the following parts of Xero API access:\n\n* Coordinating the OAuth layer to provide secure access.\n* Automatically refreshing expired tokens (for Partner Applications).\n* Parsing the response into general nested objects.\n\nThis package leaves these functions for other packages to handle, thought\ndoes coordinate them:\n\n* All HTTP communications through [Guzzle 6](https://github.com/guzzle/guzzle).\n* OAuth request signing to\n  [Guzzle OAuth Subscriber](https://github.com/guzzle/oauth-subscriber)\n* OAuth authentication recommended through\n  [OAuth 1.0 Client](https://github.com/thephpleague/oauth1-client)\n* Xero provider for OAuth 1.0 Client recommended using\n  [Xero Provider for The PHP League OAuth 1.0 Client](https://github.com/Invoiced/oauth1-xero)\n* Storage of the OAuth tokens to your own application.\n  A hook is provided so that refreshed tokens can be updated in storage.\n* Knowledge of how to navigate the results is left with your application.\n  However, the generic nested data object that the response builds, helps to do this.\n\nThis package differs from the excellent [calcinai/xero-php](https://github.com/calcinai/xero-php)\npackage in the following fundamental ways:\n\n* It does not get involved in the process of OAuth authorisation with the end user.\n  You need to handle that yourself.\n* It does not have fixed models for the responses, but uses a generic model structure\n  for resources and collections of resources.\n* The UK Payroll v2.0 API is supported by this package.\n\nWhich package best suites you, will depend on your use-case. Each have pros and cons.\n\nThis package needs the OAuth token and secret gained through authorisation\nto access the API, and the session handler token if automatic refreshing is\nneeded for the Partner Application.\n\nThis package does not care what you use at the front end to obtain those tokens.\nThe two packages recommended above to do this are reliable, well documented,\nand focus on just getting that one job done.\n\nI am mostly focusing on getting this working for the Xero Partner app, as I need\na robust librayr that just keeps on running as a scheduled process without losing\nthe tokens and needing a user to re-authenticate.\nOnce a Partner app has been authorised, it\nshould in theory be able to access the Xero account for 10 years, refreshing every\n30 minutes. In reality, tokens will get lost - even Xero has downtime that can\nresult in lost authentication tokens.\n\nAreas to Complete (TODO)\n------------------------\n\n* So far development of this package has concentrated on reading from the Xero API.\n  Writing to the API should be supported, but has not gone through any testing\n  at this stage.\n* Lots more documentation and examples.\n* More consistent handling of errors. The application should not have to go huntiong\n  to find out if the error is in the configuration, the network, the remote application,\n  the syntax of the request etc. Those details should all be handed to the application\n  on a plate.\n\nQuick Start\n-----------\n\n```php\nuse use Academe\\XeroPHP;\n\n// Most of the configuration goes into one place.\n\n$myStorageObject = /* Your OAuth token storage object */\n\n$clientProvider = new XeroPHP\\ClientProvider([\n    // Account credentials.\n    'consumerKey'       =\u003e 'your-consumer-key',\n    'consumerSecret'    =\u003e 'your-consumer-sectet',\n    // Current token and secret from storage.\n    'oauthToken'            =\u003e $myStorageObject-\u003eoauthToken,\n    'oauthTokenSecret'      =\u003e $myStorageObject-\u003eoauthTokenSecret,\n    // Curresnt session for refreshes also from storage.\n    'oauthSessionHandle'    =\u003e $myStorageObject-\u003eoauthSessionHandle,\n    // The local time the OAuth token is expected to expire.\n    'oauthExpiresAt'        =\u003e $myStorageObject-\u003eoauthExpiresAt, // Carbon, Datetime or string\n    // Running the Partner Application\n    'oauth1Options' =\u003e [\n        'signature_method' =\u003e \\GuzzleHttp\\Subscriber\\Oauth\\Oauth1::SIGNATURE_METHOD_RSA, // Default\n        'private_key_file' =\u003e 'local/path/to/private.pem',\n        'private_key_passphrase' =\u003e 'your-optional-passphrase', // Optional\n    ],\n    'clientOptions' =\u003e [\n        // You will almost always want exceptions off, so Guzzle does not throw an exception\n        // on every non-20x response.\n        // false is the default if not supplied.\n        'exceptions' =\u003e false,\n        'headers' =\u003e [\n            // We would like JSON back for most APIs, as it is structured nicely.\n            // Exceptions include 'application/pdf' to download or upload files.\n            // JSON is the default if not supplied.\n            'Accept' =\u003e XeroPHP\\ClientProvider::HEADER_ACCEPT_JSON,\n        ],\n    ],\n    // When the token is automatically refreshed, then this callback will\n    // be given the opportunity to put it into storage.\n    'tokenRefreshCallback' =\u003e function($newClientProvider, $oldClientProvider) use ($myStorageObject) {\n        // The new token and secret are available here:\n        $oauthToken= $newClientProvider-\u003eoauthToken;\n        $oauthTokenSecret = $newClientProvider-\u003eoauthTokenSecret;\n        $oauthExpiresAt = $newClientProvider-\u003eoauthExpiresAt; // Carbon\\Carbon\n\n        // Now those new credentials need storing.\n        $myStorageObject-\u003estoreTheNewTokenWhereever($oauth_token, $oauth_token_secret, $oauthExpiresAt);\n    },\n    // If you want to force a token refresh immediately, then set this option.\n    //'forceTokenRefresh' =\u003e true,\n]);\n\n// Get a plain Guzzle client, with appropriate settings.\n// Can pass in an options array to override any of the options set up in\n// the `$clientProvider`.\n\n$refreshableClient = $clientProvider-\u003egetRefreshableClient();\n```\n\nNow we have a client to send requests.\nThis is a refreshable client, so if using the Xero Partner app, it will refresh its token\nautomatically when it expires and inform your application via the `tokenRefreshCallback`.\n\nAfter sending a request, you can check if the token was refreshed for any action you may\nwant to perform as a result:\n\n```php\nif ($refreshableClient-\u003etokenIsRefreshed()) {\n    // Maybe save the token or other details, or just log the event.\n}\n```\n\nIf the token is refreshed, then the new token will have been stored by your app through\nthe `tokenRefreshCallback`.\n\nIf you want to refresh the tokens explicitly, before you hit an expired token response,\nthen it can be done like this:\n\n```php\n// Refresh the token and get a new provider back:\n\n$clientProvider = $refreshableClient-\u003erefreshToken();\n\n// Use the new $clientProvider if you want to create additional refreshable clients.\n// Otherwise just keep using the current $refreshableClient.\n\n// The `$refreshableClient` will now have a new Guzzzle client with a refreshed token.\n// The new token details are retrieved from the provider, and can then be stored,\n// assuming your callback has not already stored it. Store these three details:\n\n$clientProvider-\u003eoauthToken;        // String\n$clientProvider-\u003eoauthTokenSecret;  // String\n$clientProvider-\u003eoauthExpiresAt;    // Carbon time\n```\n\nThat may be more convenient to do, but be aware that unless you set a guard time,\nthere may be times when you miss an expiry and the request will return an expired\ntoken error.\n\nYou may want to check that the expiry time is approaching on each run, and renew the\ntoken explicitly. This check can be used to see if we have entered a \"guard window\"\npreceding the expected expiry time:\n\n```php\n// A guard window of five minutes (300 seconds).\n// If we have entered the last 300 seconds of the token lifetime,\n// then renew it immediately.\n\nif ($refreshableClient-\u003eisExpired(60*5)) {\n    $refreshableClient-\u003erefreshToken();\n}\n```\n\nThe Response Message\n--------------------\n\n### Message Instantiation\n\nThe `ResponseMessage` class is instantiated with the response data.\nEither the `Response` object or the data extracted from the response can be\nused to initialise the `ResponseMessage`:\n\n```php\n// Get the first page of payruns.\n// This assumes the payrun Endpoint was supplied as the default endpoint:\n$response = $refreshableClient-\u003eget('payruns', ['query' =\u003e ['page' =\u003e 1]]);\n\n// or if no default endpoint was given in the config:\n$response = $refreshableClient-\u003eget(\n    XeroPHP\\Endpoint::createGbPayroll('payruns')-\u003egetUrl(),\n    ['query' =\u003e ['page' =\u003e 1]]\n);\n\n// Assuming all is fine, parse the response to an array.\n$bodyArray = XeroPHP\\Helper::parseResponse($response);\n\n// Instantiate the response data object.\n$result = new XeroPHP\\ResponseMessage($bodyArray);\n\n// OR just use the PSR-7 response without the need to parse it first:\n$result = new XeroPHP\\ResponseMessage($response);\n\n### Navigating the Response Message\n\n// Now we can navigate the data.\n\n// At the top level will be metadata.\n\necho $result-\u003egetMetadata()-\u003eid;\n// 14c9fc04-f825-4163-a0cf-3c2bc31c989d\n\necho $result-\u003egetPagination()-\u003epageSize;\n// 100\n\nvar_dump($result-\u003egetPagination()-\u003etoArray());\n// array(4) {\n//   [\"page\"]=\u003e\n//   int(1)\n//   [\"pageSize\"]=\u003e\n//   int(100)\n//   [\"pageCount\"]=\u003e\n//   int(1)\n//   [\"itemCount\"]=\u003e\n//   int(3)\n// }\n\nThe results object provides access to structured data of resources fetched from the API.\nIt is a value object, and does not provide any ORM-like functionality (e.g. you can't\nupdate it then `PUT` it back to Xero, at least not yet).\n\nA `ResponseMessage` object may contain a resource, a collection or resources, or may be empty.\nThe following methods indicate what the response contains:\n\n```php\nif ($result-\u003eisCollection()) {\n    $collection = $result-\u003egetCollection();\n}\n\nif ($result-\u003eisResource()) {\n    $resource = $result-\u003egetResource();\n}\n\nif ($result-\u003eisEmpty()) {\n    // No resources - check the metadata to find out why (TODO).\n}\n```\n\n### Response Collections\n\n// Inside the ResponseMessage will be either a resource or a collection\n// of resources.\n// If it contains a single resource, then you can still extract it as a\n// collection, which will then contain a single resource.\n\nforeach($result-\u003egetCollection() as $payrun) {\n    echo $payrun-\u003eid . \" at \" . $payrun-\u003eperiodStartDate . \"\\n\";\n}\n// e4df31c9-07db-47d5-a415-6ee32d9048eb at 2017-09-25 00:00:00\n// fbd6fc76-dbfc-459d-b230-80334d175048 at 2017-10-20 00:00:00\n// 46200d03-67f2-4f5d-8852-cdad50cbe886 at 2017-10-25 00:00:00\n```\n\nThere may be further collections of resources deeper in the data, such as\na list of addresses for a contact.\n\n### Response Dates and Times\n\nAn attempt is made to convert all dates and times to a `Carbon` datetime.\nXero mixes quite a number of date formats across its APIs, so it is helpful to\nget them all normallised.\nFormats I've found so far:\n\n* \"/Date(1509454062181)/\" - milliseconds since the Unix epoch, UTC.\n* \"/Date(1439813704613+0000)/\" - milliseconds since the Unix epoch, with a timezone offset.\n* \"2017-10-20T16:04:50\" - ISO UTC time, to the second.\n* \"2017-10-31T12:50:15.9920037\" - ISO UTC timestamp with microseconds.\n* \"2017-09-25T00:00:00\" - ISO UTC date only.\n\nI'm sure there will be more. These fields are recognised solely through the suffix to\ntheir name at present. Suffixes recognised are:\n\n* UTC\n* Date\n* DateTime\n* DateOfBirth (as a prefix)\n\n### Pagination\n\nThere is no automatic pagination feature (automatically fetching subsequent pages) when\niterating over a paginated resource.\nA decorator class could easily do this though, and that may make a nice addition to take\nthe logic of \"fetching all the matching things\" that span more than one page away from\nthe application (ideally the application would make a query, then loop over the resources\nand each page would be lazy-loaded into the collection automatically when going off the\nend of the page).\n\nAll other datatypes will be either a scalar the API supplied (string, float, int, boolean)\nor another `ResponseData` object containing either a single `Resource` (e.g. \"Invoice\")\nor a `ResourceCollection` (e.g. \"CreditNotes\").\n\n### Resource Properties\n\nAccessing properties of a resource object is case-insensitive.\nThis decision was made due to the mixed use of letter cases throughout the Xero APIs.\n\nA resource will have properties. Each property may be another resource, a resource\ncollection, a date or time, or a scalar (string, integer, float).\n\nAccessing a non-existant property will return an empty `Resource`.\nDrilling deeper into an empty resource will give you further empty resources.\n\n```php\n$value = $result-\u003efoo-\u003ebar-\u003ewhere-\u003eamI;\nvar_dump($value-\u003eisEmpty());\n// bool(true)\n```\n\nBut do be aware that when you hit a scalar (e.g. a string) then that is what you will get\nback and not a `Resource` object.\n\nThe API sometimes returns a `null` for a field or resource rather than simply omitting the\nfield. Examples are the `pagination` field when fetching a single `payrun`, or the `problem`\nfield when there is no problem.\nIn this case, when you fetch the value, you will be given an empty `Resource` object\ninstead.\n\nGuzzle Exceptions\n-----------------\n\nBy default, the Guzzle client will throw an exception if it receives a non 20x HTTP response.\nWhen this happens, the HTTP response can be found in the exception.\n\n```php\ntry {\n    $response = $client-\u003eget('PayRuns', []);\n} catch (\\Exception $e) {\n    $response = $e-\u003egetResponse();\n    ...\n}\n```\n\nHandling non-20x messages like this may not be convenient, so Guzzle can be told not to\nthrow an exception using the `exceptions` option:\n\n```php\n$response = $client-\u003eget('PayRuns', ['exceptions' =\u003e false]);\n```\n\n*Note: this package now switches off Guzzle exceptions by defaul. You can turn them back on\nusing this parameter if that is desirable. Token refreshing will work both with or without\nexcpetions being enabled.*\n\nThis option can be used on each request, or set as the default in the `ClientProvider`\ninstantiation options.\nThis package is designed not to care which approach you take. However, *not* throwing an\nexception often makes sense, because even non-20x responses nearly always contain a response\nbody with information the application is going to need to log or to make a decision.\n\nCatching Errors\n---------------\n\nThere are numerous sources of error, and these are reported in many different ways, and with\ndifferent data structures. The aim of this package will be to try to normalise them, but\nin the meantime here is a list of those we know:\n\n* OAuth errors\n* Request construction errors, such as an invalid UUID format\n* Invalid resource errors, such as a missing resource or incorrect URL\n* Data errors such as trying to retrieve beyond the last page of results\n\nThe places where error details cna be found are:\n\n* OAuth errors will be returned as URL-encoded parameters in the response body.\n  The `OAuthParams` class can parse these details and provide some interpretation.\n* Request construction errors are returned TBC\n\nAPI Response Structures\n-----------------------\n\nEach response will be in one of a number of structures.\nThe structures listed below have been identified so far, with the aim that all will\nbe recognised automatically and normalised to a single resource or collection.\n\n### A: Single metadata header; single resource\n\nThe resource is in a single node, usually named after the resource content, but not always.\nExamples include fetching a single Payrun in the GB Payroll v2.0 API.\n\n\u003cimg src=\"https://github.com/academe/XeroPHP/raw/master/docs/images/A.png\" alt=\"Response Format A\" width=\"270\"\u003e\n\n### B: Single metadata header; collection of resources\n\nThe resources are in an array under a single node, usually named after the resource content, but not always.\nExamples include fetching a multiple Payruns in the GB Payroll v2.0 API.\nPaging metadata is in an object of its own.\n\n\u003cimg src=\"https://github.com/academe/XeroPHP/raw/master/docs/images/B.png\" alt=\"Response Format B\" width=\"270\"\u003e\n\n### C: Single metadata header; collection of a single resource\n\nSome APIs will return both a single resource and multiple resources in an array, with no paging metdata.\nDistinguishing between the response to requesting a single resource, or matching a single resource in\na resource collection, is not possible; the response looks the same withou looking at more details of the\ncontent of the resource.\nExamples include fetching a single payment from the Accounting v2.0 API.\n\n\u003cimg src=\"https://github.com/academe/XeroPHP/raw/master/docs/images/C.png\" alt=\"Response Format C\" width=\"270\"\u003e\n\n### D: Single metadata header; collection of resources\n\nThis structure includes some metadata at the root node, including paging details that are not\nwrapped into an object.\nExamples include fetching a multiple files from the Files v1.0 API.\n\n\u003cimg src=\"https://github.com/academe/XeroPHP/raw/master/docs/images/D.png\" alt=\"Response Format D\" width=\"270\"\u003e\n\n### E: Array of resources\n\nSome APIs will return an array of resources with no metadata at all, no paging details, no source details.\n\n\u003cimg src=\"https://github.com/academe/XeroPHP/raw/master/docs/images/E.png\" alt=\"Response Format E\" width=\"270\"\u003e\n\n### F: Single resources\n\nSimilar to format E, the response contains a single resource, not wrapped into a field or object,\nand with no metadata to provide context.\n\n\u003cimg src=\"https://github.com/academe/XeroPHP/raw/master/docs/images/F.png\" alt=\"Response Format F\" width=\"270\"\u003e\n\nThere are a number of different formats used to deliver error messages and exceptions (at least four different\nstructures). These will be documented shortly, as they need to be handled using the same rules.\n\nI suspect at least two of these stcutures can be merged into one.\n\nOther Notes\n-----------\n\nI have noticed the occassional 401 which then works on a retry. Using the Guzzle retry\nhandler would be a good move to avoid unnecessary errors when processing large amounts\nof data.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Facademe%2Fxerophp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Facademe%2Fxerophp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Facademe%2Fxerophp/lists"}