initial version, implemented StatusApi

This commit is contained in:
2025-10-09 00:30:38 +02:00
commit 86b1e39360
16 changed files with 977 additions and 0 deletions

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
vendor/
play.php

1
README.md Normal file
View File

@@ -0,0 +1 @@
# PHP-API-Client for Dolibarr ERP

21
composer.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "magdev/dolibarr-api",
"description": "API-Client for Dolibarr ERP",
"type": "library",
"license": "MIT",
"minimum-stability": "stable",
"authors": [
{
"name": "magdev",
"email": "magdev3.0@gmail.com"
}
],
"require": {
"symfony/http-client": "^7.3"
},
"autoload": {
"psr-4": {
"Magdev\\DolibarrApi\\": "src/"
}
}
}

530
composer.lock generated Normal file
View File

@@ -0,0 +1,530 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "605899efcc3f582b7c86103aad0bbaf9",
"packages": [
{
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/log",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
"time": "2024-09-11T13:17:53+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/http-client",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62",
"reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
"symfony/polyfill-php83": "^1.29",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"amphp/amp": "<2.5",
"amphp/socket": "<1.1",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "3.0"
},
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/amphp-http-client-meta": "^1.0|^2.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/rate-limiter": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.3.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-29T11:18:49+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php83\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-07-08T02:45:35+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
"reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-25T09:37:31+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

0
src/.gitignore vendored Normal file
View File

36
src/Api/AbstractApi.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
namespace Magdev\DolibarrApi\Api;
use Symfony\Contracts\HttpClient\HttpClientInterface;
abstract class AbstractApi
{
public const string PATH_PREFIX = '/api/index.php';
protected ?HttpClientInterface $client = null;
public function __construct(?HttpClientInterface $client = null)
{
if ($client instanceof HttpClientInterface) {
$this->setClient(client: $client);
}
}
public function setClient(HttpClientInterface $client): static
{
$this->client = $client;
return $this;
}
protected function getClient(): ?HttpClientInterface
{
return $this->client;
}
protected function getPath(string $path): string
{
return self::PATH_PREFIX.$path;
}
}

10
src/Api/ApiInterface.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace Magdev\DolibarrApi\Api;
use Symfony\Contracts\HttpClient\HttpClientInterface;
interface ApiInterface
{
public function setClient(HttpClientInterface $client): static;
}

38
src/Api/StatusApi.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace Magdev\DolibarrApi\Api;
use Magdev\DolibarrApi\DolibarrApiClientInterface;
use Magdev\DolibarrApi\DolibarrApiException;
use Magdev\DolibarrApi\Model\ModelInterface;
use Magdev\DolibarrApi\Model\StatusModel;
class StatusApi extends AbstractApi implements ApiInterface
{
public function getStatus(): ?ModelInterface
{
if ($client = $this->getClient()) {
$response = $client->request(method: 'GET', url: $this->getPath(path: '/status'));
if ($response->getStatusCode() == 200) {
$result = json_decode($response->getContent(), true);
if (isset($result['success'])) {
return new StatusModel($result['success']);
}
throw new DolibarrApiException(
message: sprintf('Invalid response: %s', $response->getContent()),
code: DolibarrApiClientInterface::E_INVALID_RESPONSE
);
}
throw new DolibarrApiException(
message: sprintf('Response Code %s from %s', $response->getStatusCode(), __METHOD__),
code: DolibarrApiClientInterface::E_INVALID_RESPONSE
);
}
return null;
}
}

104
src/DolibarrApiClient.php Normal file
View File

@@ -0,0 +1,104 @@
<?php
namespace Magdev\DolibarrApi;
use Magdev\DolibarrApi\Api\StatusApi;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class DolibarrApiClient implements DolibarrApiClientInterface
{
public const string APIKEY_HEADER = 'DOLAPIKEY';
private ?HttpClientInterface $client = null;
private ?string $token = null;
private ?string $baseUri = null;
private bool $verifyHttps = true;
public function __construct(?HttpClientInterface $client = null, ?string $baseUri = null, ?string $token = null, bool $verifyHttps = true)
{
$this->setClient(client: $client)
->setToken(token: $token)
->setBaseUri(baseUri: $baseUri)
->setVerifyHttps(verifyHttps: $verifyHttps);
}
public function setToken(?string $token = null): static
{
$this->token = $token;
return $this;
}
public function setBaseUri(?string $baseUri = null): static
{
$this->baseUri = $baseUri;
return $this;
}
public function setClient(?HttpClientInterface $client = null): static
{
$this->client = $client;
return $this;
}
public function status(): StatusApi
{
return new StatusApi(client: $this->getClient());
}
private function getBaseUri(): string
{
return $this->baseUri;
}
private function getClient(): HttpClientInterface
{
if (!$this->baseUri) {
throw new DolibarrApiClientException(
message: 'The property baseUri is mandatory',
code: DolibarrApiClientInterface::E_INVALID_VALUE
);
}
if (!$this->token) {
throw new DolibarrApiClientException(
message: 'The property token is mandatory',
code: DolibarrApiClientInterface::E_INVALID_VALUE
);
}
if (!$this->client) {
$this->client = HttpClient::createForBaseUri(baseUri: $this->getBaseUri(), defaultOptions: [
'headers' => [
self::APIKEY_HEADER => $this->token,
'Accept' => 'application/json',
],
'verify_host' => $this->verifyHttps,
'verify_peer' => $this->verifyHttps,
]);
return $this->client;
}
return $this->client->withOptions(options: [
'base_uri' => $this->getBaseUri(),
'headers' => [
self::APIKEY_HEADER => $this->token,
'Accept' => 'application/json',
],
'verify_host' => $this->verifyHttps,
'verify_peer' => $this->verifyHttps,
]);
}
public function setVerifyHttps(?bool $verifyHttps): self
{
$this->verifyHttps = $verifyHttps;
return $this;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Magdev\DolibarrApi;
class DolibarrApiClientException extends \RuntimeException
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Magdev\DolibarrApi;
use Magdev\DolibarrApi\Api as API;
use Symfony\Contracts\HttpClient\HttpClientInterface;
interface DolibarrApiClientInterface
{
public const int E_INVALID_VALUE = -10;
public const int E_INVALID_RESPONSE = -11;
public function setToken(?string $token = null): static;
public function setBaseUri(?string $baseUri = null): static;
public function setClient(?HttpClientInterface $client = null): static;
public function status(): API\StatusApi;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Magdev\DolibarrApi;
class DolibarrApiException extends \RuntimeException
{
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Magdev\DolibarrApi\Model;
abstract class AbstractModel
{
protected ?int $statusCode = null;
protected function toCamelCase(string $value): string
{
return str_replace(
' ',
'',
ucwords(str_replace(
['-', '_'],
' ',
$value
))
);
}
protected function toSnakeCase(string $value): string
{
return strtolower(
preg_replace(
'/(?<!^)[A-Z]/',
'_$0',
$value
)
);
}
public function getStatusCode(): ?int
{
return $this->statusCode;
}
public function setStatusCode(int $statusCode): static
{
$this->statusCode = $statusCode;
return $this;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Magdev\DolibarrApi\Model;
interface ModelInterface
{
public function getStatusCode(): ?int;
public function setStatusCode(int $statusCode): static;
public function fromArray(array $data): static;
public function toArray(): array;
}

133
src/Model/StatusModel.php Normal file
View File

@@ -0,0 +1,133 @@
<?php
namespace Magdev\DolibarrApi\Model;
use DateTimeImmutable;
use DateTimeZone;
class StatusModel extends AbstractModel implements ModelInterface
{
private ?string $dolibarVersion = null;
private ?bool $accessLocked = null;
private ?string $environment = null;
private ?DateTimeImmutable $timestampNowUtc = null;
private ?DateTimeZone $timestampPhpTz = null;
private ?DateTimeImmutable $dateTz = null;
public function __construct(?array $data = null)
{
if (is_array($data)) {
$this->fromArray(data: $data);
}
}
public function fromArray(array $data): static
{
$this->setDolibarrVersion(dolibarVersion: $data['dolibarr_version'])
->setAccessLocked(accessLocked: (bool) $data['access_locked'])
->setEnvironment(environment: $data['environment'])
->setTimestampNowUtc(timestampNowUtc: new DateTimeImmutable('@'.$data['timestamp_now_utc'], new DateTimeZone('UTC')))
->setDateTz(dateTz: $data['date_tz'])
->setTimestampPhpTz(timestampPhpTz: $data['timestamp_php_tz'])
->setStatusCode(statusCode: $data['code'])
;
return $this;
}
public function toArray(): array
{
return [
'dolibarr_version' => $this->getDolibarrVersion(),
'access_locked' => (int) $this->isAccessLocked(),
'environment' => $this->getEnvironment(),
'timestamp_now_utc' => $this->getTimestampNowUtc(),
'timestamp_php_tz' => $this->getTimestampPhpTz(),
'date_tz' => $this->getDateTz(),
'code' => $this->getStatusCode(),
];
}
public function getDolibarrVersion(): ?string
{
return $this->dolibarVersion;
}
public function setDolibarrVersion(?string $dolibarVersion): self
{
$this->dolibarVersion = $dolibarVersion;
return $this;
}
public function isAccessLocked(): ?bool
{
return $this->accessLocked;
}
public function setAccessLocked(?bool $accessLocked): self
{
$this->accessLocked = $accessLocked;
return $this;
}
public function getEnvironment(): ?string
{
return $this->environment;
}
public function setEnvironment(?string $environment): self
{
$this->environment = $environment;
return $this;
}
public function getTimestampNowUtc(): ?DateTimeImmutable
{
return $this->timestampNowUtc;
}
public function setTimestampNowUtc(DateTimeImmutable|string|null $timestampNowUtc): self
{
if (is_string($timestampNowUtc)) {
$timestampNowUtc = new DateTimeImmutable($timestampNowUtc, new DateTimeZone('UTC'));
}
$this->timestampNowUtc = $timestampNowUtc;
return $this;
}
public function getTimestampPhpTz(): ?DateTimeZone
{
return $this->timestampPhpTz;
}
public function setTimestampPhpTz(DateTimeZone|string|null $timestampPhpTz): self
{
if (is_string($timestampPhpTz)) {
$timestampPhpTz = new DateTimeZone($timestampPhpTz);
}
$this->timestampPhpTz = $timestampPhpTz;
return $this;
}
public function getDateTz(): ?DateTimeImmutable
{
return $this->dateTz;
}
public function setDateTz(DateTimeImmutable|string|null $dateTz): self
{
if (is_string($dateTz)) {
$dateTz = new DateTimeImmutable($dateTz);
}
$this->dateTz = $dateTz;
return $this;
}
}