Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/.env.example export-ignore
/.gitattributes export-ignore
/.github export-ignore
/.gitignore export-ignore
/.php-cs-fixer.php export-ignore
/phpstan.neon export-ignore
/phpunit.xml export-ignore
/tests export-ignore
29 changes: 29 additions & 0 deletions .github/scripts/php74-smoke.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

// Runtime smoke test for the minimum supported PHP version (7.4).
// Uses only production dependencies: Guzzle's MockHandler stands in for the network.
// Usage: php php74-smoke.php <path to the consumer project's vendor/autoload.php>

declare(strict_types=1);

require $argv[1];

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Response;
use ready2order\Client;

$mock = new MockHandler([new Response(200, [], '{"company_name":"ACME"}')]);
$client = new Client('token', 'http://smoke.test/v1', new GuzzleClient(['handler' => $mock]));

if ($client->setTimeout(5)->setLanguage('de-DE')->getTimeout() !== 5) {
fwrite(STDERR, "setTimeout/getTimeout failed\n");
exit(1);
}

if ($client->get('company') !== ['company_name' => 'ACME']) {
fwrite(STDERR, "get() returned unexpected data\n");
exit(1);
}

echo 'OK on PHP ' . PHP_VERSION . "\n";
124 changes: 124 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: CI

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
checks:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
php: ['8.3', '8.4']
steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl, json, mbstring
coverage: none

- run: composer validate --strict

- run: composer install --no-interaction --prefer-dist

- name: Code style
run: composer lint:check

- name: Static analysis
run: composer analyse

# Unit tests use mocked HTTP; no network or token needed.
- name: Unit tests
run: composer test

# composer.json claims PHP >=7.4, but the dev dependencies (PHPUnit 12) need
# 8.3, so the test suite cannot run there. Instead install the library into a
# throwaway project like a consumer would (only `require` is resolved), then
# lint it and run a smoke test.
php74:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
extensions: curl, json, mbstring
coverage: none

- name: Lint src
run: find src -name '*.php' -print0 | xargs -0 -n1 php -l

- name: Install as a consumer would
run: |
mkdir "$RUNNER_TEMP/consumer"
cd "$RUNNER_TEMP/consumer"
composer init --no-interaction --name=smoke/consumer
composer config repositories.lib path "$GITHUB_WORKSPACE"
composer require --no-interaction "ready2order/r2o-api-client-php:@dev"

- name: Smoke test
run: php .github/scripts/php74-smoke.php "$RUNNER_TEMP/consumer/vendor/autoload.php"

# The library allows Guzzle ^7.16 || ^8.3 but `checks` resolves the newest (8.x).
# Pin the oldest supported 7.x and re-run the unit tests against it.
guzzle7:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: curl, json, mbstring
coverage: none

- run: composer install --no-interaction --prefer-dist

- run: composer require --no-interaction --with-all-dependencies "guzzlehttp/guzzle:7.16.0"

- name: Unit tests (Guzzle 7)
run: composer test

# Hits the real API with a test-account token; creates and deletes data.
# Runs only after the static checks pass, so a commit that fails lint never
# touches the real account. Skipped for fork PRs (no secrets). The concurrency
# group makes runs queue instead of overlapping on the same account; GitHub
# keeps only one pending run per group, so a newer queued run replaces an
# older pending one.
live-api:
Comment thread
r2omvavra marked this conversation as resolved.
Resolved
needs: checks
timeout-minutes: 10
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
concurrency:
group: live-api
cancel-in-progress: false
steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: curl, json, mbstring
coverage: none

- run: composer install --no-interaction --prefer-dist

# --fail-on-skipped: an unset secret/variable must fail the job, not pass it with every test skipped.
- name: Live API tests
run: composer test:api -- --fail-on-skipped
env:
R2O_API_TOKEN: ${{ secrets.R2O_API_TOKEN }}
R2O_API_ENDPOINT: ${{ vars.R2O_API_ENDPOINT }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.env
/vendor
composer.lock
.php_cs.cache
.php-cs-fixer.cache
.phpunit.result.cache
.claude
CLAUDE.md
1 change: 0 additions & 1 deletion .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,4 @@
'method_argument_space' => ['on_multiline' => 'ignore'],
'no_superfluous_elseif' => false,
])
->setCacheFile(sys_get_temp_dir() . '/php_cs.cache')
->setFinder($finder);
77 changes: 69 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ $newProduct = $client->post('products', [
'product_price' => '3.50',
]);

// PUT - Create or replace resources
$productGroup = $client->put('productgroups', [
'productgroup_name' => 'Beverages',
// PUT - Update resources
$updated = $client->put('products/123', [
'product_price' => '4.00',
]);

// PATCH - Update resources
// PATCH - Partially update resources
$updated = $client->patch('products/123', [
'product_price' => '4.00',
]);
Expand Down Expand Up @@ -92,6 +92,17 @@ For testing or custom deployments, pass the endpoint as the second constructor p
$client = new Client('your-token', 'https://custom-api.example.com/v1');
```

### Custom HTTP Client

Pass your own Guzzle client as the third constructor parameter (e.g. for proxies or a custom `User-Agent`):

```php
$guzzle = new \GuzzleHttp\Client(['headers' => ['User-Agent' => 'my-app/1.0']]);
$client = new Client('your-token', null, $guzzle);
```

The default client sends a ready2order `User-Agent`, `Accept: application/json` and `Cache-Control: no-cache`; an injected client keeps its own defaults. `Authorization` and `Accept-Language` are always set by `Client`.

## Error Handling

The client throws specific exceptions for different error scenarios:
Expand All @@ -101,6 +112,7 @@ use ready2order\Client;
use ready2order\Exceptions\ErrorResponseException;
use ready2order\Exceptions\ResourceNotFoundException;
use ready2order\Exceptions\InvalidResponseException;
use ready2order\Exceptions\ConnectionException;

$client = new Client('your-token');

Expand All @@ -110,7 +122,7 @@ try {
// HTTP 404 - Resource not found
echo "Product not found: " . $e->getMessage();
} catch (ErrorResponseException $e) {
// API returned an error (4xx/5xx)
// API returned an error (4xx/5xx, including non-JSON error bodies)
echo "API error: " . $e->getMessage();

// Access full error response data
Expand All @@ -119,8 +131,23 @@ try {
print_r($errorData);
}
} catch (InvalidResponseException $e) {
// Response could not be JSON-decoded
echo "Invalid response from API";
// A successful response could not be JSON-decoded
echo "Invalid response from API: " . $e->getMessage();
} catch (ConnectionException $e) {
// Request failed without an HTTP error response (DNS, refused connection, timeout, redirect loop)
echo "Could not reach API: " . $e->getMessage();
}
```

All of these extend `ready2order\Exceptions\Ready2OrderException`, so you can also handle any failed API call in one place (invalid configuration, e.g. a malformed endpoint URL, still throws a plain `InvalidArgumentException`):

```php
use ready2order\Exceptions\Ready2OrderException;

try {
$client->get('products');
} catch (Ready2OrderException $e) {
// ErrorResponseException, ResourceNotFoundException, InvalidResponseException or ConnectionException
}
```

Expand Down Expand Up @@ -175,7 +202,41 @@ echo "Created product: " . $product['product_name'];
- PHP 7.4 or higher
- ext-curl
- ext-json
- guzzlehttp/guzzle ^7.0
- ext-mbstring
- guzzlehttp/guzzle ^7.16 or ^8.3

## Upgrading

Changes compared to the previous release (breaking, so release as a new major version):

- `setApiEndpoint()` is removed; pass the endpoint as the second constructor argument.
- Failed requests no longer throw raw Guzzle exceptions. A 5xx now throws `ErrorResponseException`, a connect error, timeout or redirect loop throws `ConnectionException`, and a 4xx with a non-JSON body throws `ErrorResponseException` (with `getData() === null`) instead of `InvalidResponseException`. The Guzzle exception is available via `getPrevious()`.
- Request bodies of POST, PUT, PATCH and DELETE are sent as JSON (`Content-Type: application/json`) instead of form data.
- `setTimeout()` and `setLanguage()` return `$this` instead of nothing, and `setTimeout()` is now actually applied to requests.

## Development

Contributors need PHP 8.3 or higher to run the tests and tooling (PHPUnit 12); the library itself supports PHP 7.4+. Install dependencies with Composer, then:

```bash
composer test # unit tests, mocked HTTP, no token needed
composer lint:check # php-cs-fixer (dry run); `composer lint` applies fixes
composer analyse # phpstan
```

### Live API tests

`composer test:api` runs the `api` suite against a real ready2order API. These
tests **create and delete real data** (products, product groups) in the account
the token belongs to, so use a throwaway account. Copy `.env.example` to `.env`
and set:

- `R2O_API_TOKEN` – account token of the test account
- `R2O_API_ENDPOINT` – endpoint the tests target; `.env.example` defaults to
the production API `https://api.ready2order.com/v1`

The account needs at least one existing product (`testGetProductList` does not seed data).
Without a token the live tests are skipped; CI passes `--fail-on-skipped` so a missing secret fails the job.

## License

Expand Down
27 changes: 19 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "ready2order/r2o-api-client-php",
"description": "PHP library for ready2order POS API v1",
"keywords": ["ready2order", "api", "client", "pos", "guzzle"],
"replace": {
"duxthefux/ready2order-php-api-v1" : "1.*"
},
Expand All @@ -9,25 +10,35 @@
"php": ">=7.4",
Comment thread
r2omvavra marked this conversation as resolved.
Resolved
"ext-curl": "*",
"ext-json": "*",
"guzzlehttp/guzzle": "^7.0"
"ext-mbstring": "*",
"guzzlehttp/guzzle": "^7.16 || ^8.3"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^10.0",
"vlucas/phpdotenv": "^5.0",
"symfony/var-dumper": "^5.1 || ^6.0 || ^7.0",
"friendsofphp/php-cs-fixer": "^3.0"
"friendsofphp/php-cs-fixer": "^3.95",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.5",
"symfony/var-dumper": "^7.4",
"vlucas/phpdotenv": "^5.8"
},
"autoload": {
"psr-4": {
"ready2order\\": "src",
"ready2order\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"ready2order\\Tests\\": "tests"
}
},
"scripts": {
"lint": "php-cs-fixer fix --path-mode=intersection ."
"lint": "php-cs-fixer fix",
"lint:check": "php-cs-fixer fix --dry-run --diff",
"analyse": "phpstan analyse --memory-limit=512M",
"test": "phpunit --testsuite unit",
"test:api": "phpunit --testsuite api"
},
"homepage": "https://github.com/ready2order/r2o-api-client-php",
"authors": [
"authors": [
{
"name": "ready2order GmbH",
"email": "operations@ready2order.com",
Expand Down
6 changes: 6 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
level: 5
paths:
- src
- tests
treatPhpDocTypesAsCertain: false
7 changes: 5 additions & 2 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="ready2order API Test suite">
<directory suffix="Test.php">./tests/</directory>
<testsuite name="unit"><file>./tests/ClientUnitTest.php</file></testsuite>
<testsuite name="api">
<file>./tests/ClientTest.php</file>
<file>./tests/ErrorHandlingTest.php</file>
<file>./tests/ProductsTest.php</file>
</testsuite>
</testsuites>
</phpunit>
Loading
Loading