Skip to content

Commit 060df76

Browse files
authored
API 3.1 (#340)
* Fix API action names to match HostFact API v2 docs, remove psalm API action name fixes (verified against hostfact.nl/developer/api): - CanResendApproverEmail: resendapproveremail -> resendapprovermail - CanPaymentProcessPause: payment_process_pause -> paymentprocesspause - CanPaymentProcessReactivate: payment_process_reactivate -> paymentprocessreactivate - CanAddAttachment/CanDeleteAttachment/CanDownloadAttachment: use controller=attachment with action=add/delete/download - CanAddLine/CanDeleteLine: use controller={name}line with action=add/delete - Hosting: new CanEmailAccountInfo trait sending sendaccountinfobyemail (was incorrectly sharing VPS trait sending sendaccountdatabyemail) - Api.php: store exception message string instead of non-serializable exception object in error array - LICENSE: fix copyright year typo (2105 -> 2015) - Remove psalm (no modern PHP support): config, dependency, and script references * Update PHP version requirements to supported versions only * Update all dependencies to latest major versions Production: - hyperized/value-objects: ^0.3.0 -> ^1.0.0 (update ByteArrayInterface namespace) - thecodingmachine/safe: ^2.4 -> ^3.4 Dev: - phpunit/phpunit: ^9.5 -> ^13.0 (update phpunit.xml.dist to v13 schema) - orchestra/testbench: ^7.15||^8.0 -> ^11.0 - phpstan/phpstan: ^1.9 -> ^2.0 - infection/infection: ^0.26.16||^0.27.0 -> ^0.32 * Update GitHub workflow actions and dependabot config - actions/checkout: v1 -> v4 - Remove deprecated --no-suggest flag and unnecessary composer self-update step - Add github-actions ecosystem to dependabot for automatic action updates - Use local PHPUnit XSD schema reference for infection compatibility * Remove deprecated delete_head_branch from mergify config The delete_head_branch action is deprecated by Mergify (deadline: 2026-07-31). Use GitHub's native 'Automatically delete head branches' setting instead. Supersedes #339. * Update dependencies and drop PHP 8.2 from CI * Add typed response system with DataBag, ApiResponse subclasses, and ResponseFactory * Refactor API layer to return typed responses * Add typed entity classes for HostFact API v3.1 * Add tests, examples, and updated documentation * Add backed enums for HostFact API value sets 18 enums: Sex, Periodic, ProductType, InvoiceMethod, VatCalcMethod, DiscountPercentageType, GroupType, TicketType, TicketPriority, InvoiceStatus, InvoiceSubStatus, OrderStatus, PriceQuoteStatus, DomainStatus, HostingStatus, SslStatus, VpsStatus, TicketStatus. * Add nullableBool, nullableDateTime, and nullableEnum helpers DataBag gains nullableBool() (yes/no/1/0/true/false) and nullableDateTime() methods. Entity base class gains nullableEnum() using ReflectionEnum for int/string backing detection. * Replace string properties with strict types in all entity classes Identifiers become ?int, dates become ?DateTimeImmutable, booleans become ?bool, finite value sets become enums. Prices stay ?string for bcmath precision. Update test assertions to match. * Update docs and examples to use typed entities * Add tests for Group, Order, PriceQuote, and Ticket entities * Add tests for ServiceProvider::provides and Facade accessor * Enable prefer-lowest in CI matrix * Reduce extractErrors cyclomatic complexity to satisfy PHPMD * Bump phpmd/phpmd to ^2.14 for readonly class support * Update composer.lock * Drop PHP 8.3, require PHP 8.4+ * Add #[\Override] to entity fromBag() methods * Raise PHPMD cyclomatic complexity threshold to 10 * Bump hyperized/value-objects to ^2.0.0 * Remove thecodingmachine/safe, use native JSON_THROW_ON_ERROR * Preserve controller/action in JSON parse error responses * Use ReflectionNamedType for enum backing type check * Prevent credential leaks in exception messages * Fix dependabot typo in mergify config * Remove phpcbf from CI, bump phpcs minimum to 3.8 PHPCBF is a fixer, not a validator — it returns exit code 1 when it fixes code, causing CI failures. PHPCS already validates style. Bump phpcs minimum to 3.8 for readonly class support.
1 parent 577e218 commit 060df76

193 files changed

Lines changed: 8691 additions & 4004 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@ updates:
77
interval: weekly
88
time: "08:00"
99
open-pull-requests-limit: 10
10+
- package-ecosystem: github-actions
11+
directory: "/"
12+
schedule:
13+
interval: weekly
14+
time: "08:00"
15+
open-pull-requests-limit: 5

.github/workflows/main.yml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
php:
13-
- 8.1
14-
- 8.2
13+
- 8.4
14+
- 8.5
1515
dependency-version:
16-
# - prefer-lowest
16+
- prefer-lowest
1717
- prefer-stable
1818
os:
1919
- ubuntu-latest
@@ -24,19 +24,16 @@ jobs:
2424
runs-on: ${{ matrix.os }}
2525
steps:
2626
- name: Checkout code
27-
uses: actions/checkout@v1
27+
uses: actions/checkout@v4
2828
- name: Setup PHP
2929
uses: shivammathur/setup-php@v2
3030
with:
3131
php-version: ${{ matrix.php }}
3232
extensions: curl, json
3333
coverage: pcov
34-
- name: Composer self-update
35-
run: |
36-
composer self-update
3734
- name: Composer update
3835
run: |
39-
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
36+
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
4037
- name: Composer ${{ matrix.test }}-${{ matrix.os }}
4138
run: |
4239
composer ${{ matrix.test }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/.idea/
33
clover.xml
44
.phpunit.result.cache
5+
CLAUDE.md

.mergify.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,10 @@ pull_request_rules:
77
type: APPROVE
88
message: Automatically approving dependabot
99

10-
- name: automatic merge on depenabot pull requests
10+
- name: automatic merge on dependabot pull requests
1111
conditions:
1212
- author=dependabot[bot]
1313
- check-success~=^test*
1414
actions:
1515
merge:
1616
method: merge
17-
18-
- name: delete head branch after merge
19-
conditions:
20-
- merged
21-
actions:
22-
delete_head_branch:

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2105 Gerben Geijteman
3+
Copyright (c) 2015 Gerben Geijteman
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 161 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,202 @@
1-
# Hostfact API 3.0 for Laravel
1+
# Hostfact API v3.1 for Laravel
22

3-
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fhyperized%2Fhostfact.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fhyperized%2Fhostfact?ref=badge_shield)
3+
[![Run tests](https://github.com/hyperized/hostfact/actions/workflows/main.yml/badge.svg)](https://github.com/hyperized/hostfact/actions/workflows/main.yml)
44

5-
Official documentation:
6-
-----------------------
5+
Unofficial Laravel package for the [HostFact API v2](https://www.hostfact.nl/developer/api/).
76

8-
* [Hostfact API documentation](https://www.hostfact.nl/developer/api/)
7+
## Requirements
98

10-
Installation
11-
------------
9+
- PHP 8.4+
10+
- Laravel 13 and above (auto-discovery supported)
1211

13-
Install using composer:
12+
## Installation
1413

1514
```bash
1615
composer require hyperized/hostfact
1716
```
1817

19-
This package supports Package Auto-Discovery (Laravel 5.5+) so it doesn't require you to manually add the
20-
ServiceProvider and alias.
18+
Publish the configuration:
2119

22-
If you are using a lower version of Laravel or not using Auto-Discovery you can add the Hostfact Service Provider to
23-
the `config/app.php` file
20+
```bash
21+
php artisan vendor:publish --provider="Hyperized\Hostfact\Providers\HostfactServiceProvider" --tag="config"
22+
```
2423

25-
```php
26-
Hyperized\Hostfact\HostfactServiceProvider::class,
24+
## Configuration
25+
26+
Add these to your `.env` file:
27+
28+
```env
29+
HOSTFACT_URL=https://yoursite.tld/Pro/apiv2/api.php
30+
HOSTFACT_KEY=your-api-token
31+
HOSTFACT_TIMEOUT=20
2732
```
2833

29-
Register an alias for Hostfact, also in `config/app.php`:
34+
Or edit `config/Hostfact.php` directly:
3035

3136
```php
32-
'Hostfact' => Hyperized\Hostfact\HostfactServiceProvider::class,
37+
return [
38+
'api_v2_url' => env('HOSTFACT_URL', 'https://yoursite.tld/Pro/apiv2/api.php'),
39+
'api_v2_key' => env('HOSTFACT_KEY', 'token'),
40+
'api_v2_timeout' => env('HOSTFACT_TIMEOUT', 20),
41+
];
3342
```
3443

35-
Now publish the Hostfact package into your installation:
44+
## Usage
3645

37-
```bash
38-
php artisan vendor:publish --provider="Hyperized\Hostfact\HostfactServiceProvider" --tag="config"
39-
```
46+
Every controller provides a static `new()` factory that reads configuration from Laravel automatically:
47+
48+
```php
49+
use Hyperized\Hostfact\Api\Controllers\Product;
50+
use Hyperized\Hostfact\Api\Entity\Product as ProductEntity;
51+
use Hyperized\Hostfact\Api\Response\ListResponse;
4052

41-
This should give you a message
42-
like: `Copied File [/vendor/hyperized/hostfact/config/Hostfact.php] To [/config/Hostfact.php]`
53+
$response = Product::new()->list(['searchfor' => 'hosting']);
4354

44-
It's possible to edit your configuration variables in the `config/Hostfact.php` file or you can use the `HOSTFACT_URL`
45-
and `HOSTFACT_KEY` environment variables to store sensitive information in the `.env` file
55+
if ($response instanceof ListResponse) {
56+
echo $response->pagination->totalResults . ' results';
57+
58+
foreach ($response->entities as $product) {
59+
assert($product instanceof ProductEntity);
60+
echo $product->ProductName;
61+
echo $product->PriceExcl;
62+
}
63+
}
64+
```
65+
66+
For dependency injection or testing, use `fromHttpClient()`:
4667

4768
```php
48-
// config/Hostfact.php
49-
'api_v2_url' => env('HOSTFACT_URL', 'https://yoursite.tld/Pro/apiv2/api.php'),
50-
'api_v2_key' => env('HOSTFACT_KEY', 'token'),
51-
'api_v2_timeout' => env('HOSTFACT_TIMEOUT', 20),
69+
use Hyperized\Hostfact\Api\Controllers\Invoice;
5270

53-
// .env/.env.example
54-
HOSTFACT_URL=https://yoursite.tld/Pro/apiv2/api.php
55-
HOSTFACT_KEY=token
56-
HOSTFACT_TIMEOUT=20
71+
$invoice = Invoice::fromHttpClient($httpClient);
72+
$result = $invoice->show(['Identifier' => 'F0001']);
5773
```
5874

59-
Functionality
60-
---------
75+
See the [examples/](examples/) directory for more complete usage patterns.
76+
77+
## Typed Responses
78+
79+
All API methods return an `ApiResponse` subclass:
80+
81+
| Response Type | When | Properties |
82+
|---|---|---|
83+
| `ShowResponse` | Single entity returned | `entity` (typed entity), `data` (DataBag) |
84+
| `ListResponse` | Multiple entities returned | `entities` (list of typed entities), `items` (list of DataBag), `pagination` |
85+
| `ActionResponse` | Action with no entity data (e.g. markAsPaid) ||
86+
| `ErrorResponse` | API returned an error | `errors` (list of strings) |
6187

62-
When writing code for this Hostfact package, consider that this package has been written as a basic interface.
88+
All responses share: `controller`, `action`, `status`, `date`, `isSuccess()`, `isError()`, `toArray()`.
6389

64-
This package _will_ do the following:
90+
### Typed Entities
6591

66-
* Provide an easy way to communicate with Hostfact API controllers;
67-
* Document the available API controller endpoints with methods;
68-
* Transport layer (HTTP/HTTPS) error catching;
69-
* Basic error parsing;
92+
Responses include typed entity objects with IDE autocompletion and strict PHP types:
93+
94+
```php
95+
use Hyperized\Hostfact\Api\Controllers\Invoice;
96+
use Hyperized\Hostfact\Api\Entity\Invoice as InvoiceEntity;
97+
use Hyperized\Hostfact\Api\Response\ShowResponse;
98+
99+
$response = Invoice::new()->show(['InvoiceCode' => 'F0001']);
100+
assert($response instanceof ShowResponse);
101+
102+
$invoice = $response->entity;
103+
assert($invoice instanceof InvoiceEntity);
104+
105+
$invoice->Identifier; // ?int
106+
$invoice->InvoiceCode; // ?string
107+
$invoice->Status; // ?InvoiceStatus (enum)
108+
$invoice->Date; // ?DateTimeImmutable
109+
$invoice->AmountExcl; // ?string (for bcmath precision)
110+
$invoice->Sent; // ?bool
111+
112+
// Nested entities
113+
foreach ($invoice->InvoiceLines as $line) {
114+
$line->Description; // ?string
115+
$line->PriceExcl; // ?string
116+
}
117+
118+
// Fallback to DataBag for undocumented fields
119+
$invoice->bag->string('SomeUndocumentedField');
120+
```
70121

71-
This package _will not_:
122+
For list responses:
72123

73-
* Parameter / input validation;
74-
* Output validation;
124+
```php
125+
$response = Product::new()->list(['searchfor' => 'hosting']);
126+
assert($response instanceof ListResponse);
127+
128+
foreach ($response->entities as $product) {
129+
assert($product instanceof ProductEntity);
130+
echo $product->ProductCode;
131+
echo $product->ProductName;
132+
}
133+
```
75134

76-
You will need to consult the [Hostfact API documentation](https://www.hostfact.nl/developer/api/) to understand the
77-
acceptable input and output for each of the API controllers.
135+
Available entity classes: `Product`, `Debtor`, `Invoice`, `Domain`, `Hosting`, `Ssl`, `Vps`, `Ticket`, `Order`, `PriceQuote`, `Creditor`, `Group`. Controllers without documented fields (`Service`, `CreditInvoice`, `Handle`) return a `DataBag` as the entity.
78136

79-
Examples
80-
--------
137+
### DataBag
81138

82-
Example code:
139+
The raw API data is also accessible through typed methods on `DataBag`:
83140

84141
```php
85-
use \Hyperized\Hostfact\Api\Controllers\Product;
142+
$bag->string('ProductCode') // string
143+
$bag->int('Identifier') // int
144+
$bag->float('PriceExcl') // float
145+
$bag->bool('AutoRenew') // bool (handles "yes"/"no", 1/0)
146+
$bag->nullableString('Comment') // ?string
147+
$bag->nullableInt('PackageID') // ?int
148+
$bag->nullableBool('Sent') // ?bool
149+
$bag->nullableDateTime('Created') // ?DateTimeImmutable
150+
$bag->array('Groups') // array
151+
$bag->bag('Subscription') // nested DataBag
152+
$bag->bags('InvoiceLines') // list<DataBag>
153+
$bag->has('SomeField') // bool
154+
$bag['ProductCode'] // mixed (ArrayAccess)
155+
```
156+
157+
## Available Controllers
158+
159+
| Controller | Actions |
160+
|---|---|
161+
| `CreditInvoice` | show, list, add, edit, delete, partialPayment, markAsPaid, lineAdd, lineDelete, attachmentAdd, attachmentDelete, attachmentDownload |
162+
| `Creditor` | show, list, add, edit, delete, attachmentAdd, attachmentDelete, attachmentDownload |
163+
| `Debtor` | show, list, add, edit, checkLogin, updateLoginCredentials, generatePdf, sendEmail, attachmentAdd, attachmentDelete, attachmentDownload |
164+
| `Domain` | show, list, add, edit, terminate, delete, getToken, lock, unlock, changeNameserver, syncWhois, editWhois, check, transfer, register, autoRenew, listDnsTemplates, getDnsZone, editDnsZone |
165+
| `Group` | show, list, add, edit, delete |
166+
| `Handle` | show, list, add, edit, delete, listDomain |
167+
| `Hosting` | show, list, add, edit, terminate, delete, suspend, unsuspend, create, removeFromServer, getDomainList, emailAccountData, upDowngrade |
168+
| `Invoice` | show, list, add, edit, delete, credit, partialPayment, markAsPaid, markAsUnpaid, sendByEmail, sendReminderByEmail, sendSummationByEmail, download, lineAdd, lineDelete, attachmentAdd, attachmentDelete, attachmentDownload, block, unblock, schedule, cancelSchedule, paymentProcessPause, paymentProcessReactivate |
169+
| `Order` | show, list, add, edit, process, lineAdd, lineDelete |
170+
| `PriceQuote` | show, list, add, edit, delete, sendByEmail, download, accept, decline, lineAdd, lineDelete, attachmentAdd, attachmentDelete, attachmentDownload |
171+
| `Product` | show, list, add, edit, delete |
172+
| `Service` | show, list, add, edit, terminate |
173+
| `Ssl` | show, list, add, edit, terminate, request, markAsInstalled, download, reissue, renew, getStatus, resendApproverEmail, revoke, markAsUninstalled |
174+
| `Ticket` | show, list, add, edit, delete, addMessage, changeStatus, changeOwner, attachmentDownload |
175+
| `Vps` | show, list, add, edit, terminate, create, start, pause, restart, suspend, unsuspend, downloadAccountData, emailAccountData |
176+
177+
## Design
178+
179+
This package provides a thin wrapper around the HostFact API. It does **not** validate input parameters. Consult the [HostFact API documentation](https://www.hostfact.nl/developer/api/) for accepted parameters.
180+
181+
Architecture:
182+
- **Controllers** extend the abstract `Api` class and compose capability **traits** (e.g., `CanShow`, `CanList`, `CanAdd`)
183+
- Each controller implements a corresponding **interface**
184+
- All API methods accept `array<string, mixed>` and return typed `ApiResponse` subclasses
185+
- HTTP transport is handled by Guzzle 7.x
186+
187+
## Testing
188+
189+
```bash
190+
# Full test suite (PHPMD, PHPStan, PHPCS, phpmnd, PHPUnit, Infection)
191+
composer test
192+
193+
# PHPUnit only
194+
composer phpunit -- --configuration phpunit.xml.dist
86195

87-
$products = Product::new()
88-
->list([
89-
'searchfor' => 'invoice'
90-
]);
196+
# Static analysis
197+
composer phpstan -- analyse
91198
```
92199

93200
## License
94201

95-
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fhyperized%2Fhostfact.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fhyperized%2Fhostfact?ref=badge_large)
202+
MIT - see [LICENSE](LICENSE) for details.

composer.json

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,18 @@
1616
}
1717
],
1818
"require": {
19-
"php": "^8.1",
19+
"php": "^8.4",
2020
"guzzlehttp/guzzle": "^7.5",
21-
"hyperized/value-objects": "^0.3.0",
22-
"thecodingmachine/safe": "^2.4"
21+
"hyperized/value-objects": "^2.0.0"
2322
},
2423
"require-dev": {
25-
"phpunit/phpunit": "^9.5",
26-
"phpmd/phpmd": "^2.13",
27-
"vimeo/psalm": "^5.1",
28-
"orchestra/testbench": "^7.15 || ^8.0",
29-
"phpstan/phpstan": "^1.9",
30-
"squizlabs/php_codesniffer": "^3.7 || ^4.0",
24+
"phpunit/phpunit": "^13.0",
25+
"phpmd/phpmd": "^2.14",
26+
"orchestra/testbench": "^11.0",
27+
"phpstan/phpstan": "^2.0",
28+
"squizlabs/php_codesniffer": "^3.8 || ^4.0",
3129
"povils/phpmnd": "^3.0",
32-
"infection/infection": "^0.26.16 || ^0.27.0"
30+
"infection/infection": "^0.32"
3331
},
3432
"autoload": {
3533
"psr-4": {
@@ -55,12 +53,8 @@
5553
"test": [
5654
"@phpmd --version",
5755
"@phpmd --strict src text cyclomatic.xml",
58-
"@psalm --version",
59-
"@psalm",
6056
"@phpstan --version",
6157
"@phpstan analyse",
62-
"@phpcbf --version",
63-
"@phpcbf src",
6458
"@phpcs --version",
6559
"@phpcs src --standard=PSR2",
6660
"@phpmnd --version",
@@ -75,10 +69,8 @@
7569
],
7670
"phpunit": "vendor/phpunit/phpunit/phpunit",
7771
"phpmd": "vendor/bin/phpmd",
78-
"psalm": "vendor/bin/psalm",
7972
"phpstan": "vendor/bin/phpstan",
8073
"phpcs": "vendor/bin/phpcs",
81-
"phpcbf": "vendor/bin/phpcbf",
8274
"phpmnd": "vendor/bin/phpmnd src",
8375
"infection": "vendor/bin/infection",
8476
"major": [

0 commit comments

Comments
 (0)