Skip to content

Commit 95d30fd

Browse files
committed
Added API client
1 parent 2053f1d commit 95d30fd

7 files changed

Lines changed: 267 additions & 3 deletions

File tree

.gitignore

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
composer.lock
2-
vendor/
3-
41
###> friendsofphp/php-cs-fixer ###
52
/.php-cs-fixer.cache
63
###< friendsofphp/php-cs-fixer ###
4+
5+
*.local
6+
.idea
7+
composer.lock
8+
vendor/

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,42 @@
22

33
API client for [The cBrain F2 Platform](https://www.cbrain.com/software-pages/the-f2-platform).
44

5+
## Installation
6+
7+
``` shell
8+
composer require itk-dev/f2-api-client
9+
```
10+
11+
## Testing
12+
13+
Edit `.env.local` and set these variables:
14+
15+
``` dotenv
16+
# .env.local
17+
F2_API_URI=
18+
F2_API_USERNAME=
19+
F2_API_SECRET=
20+
F2_F2_USERNAME=
21+
```
22+
23+
Run
24+
25+
``` shell
26+
docker compose run --quiet --rm phpfpm php bin/f2-api-client getServiceIndex
27+
```
28+
29+
to check that you can talk to the F2 API.
30+
31+
Search cases with
32+
33+
``` shell
34+
docker compose run --quiet --rm phpfpm php bin/f2-api-client searchCases '{"q": "test", "count": 10}'
35+
```
36+
37+
docker compose run --quiet --env PHP_XDEBUG_MODE=debug --rm phpfpm php bin/f2-api-client
38+
39+
---
40+
541
``` mermaid
642
---
743
title: F2 Conceptual model
@@ -10,6 +46,11 @@ title: F2 Conceptual model
1046
classDiagram
1147
Document --> Matter
1248
Matter --> CaseFile
49+
Party
50+
Note --> CaseFile: (only if not related to Matter)
51+
Note --> Matter: (only if not related to CaseFile)
52+
53+
Chat --> Matter
1354
1455
%% class Item{
1556
%% +String title
@@ -19,3 +60,8 @@ classDiagram
1960
%% Item <|-- Matter
2061
%% Item <|-- CaseFile
2162
```
63+
64+
65+
> The client MUST read the service index at runtime and use it to locate the links it needs.
66+
67+
<./resources/f2-rest-docs-v13.pdf#page=8>

bin/f2-api-client

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
// @see https://getcomposer.org/doc/articles/vendor-binaries.md#finding-the-composer-autoloader-from-a-binary
5+
require $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';
6+
7+
use Symfony\Component\Console\Application;
8+
9+
$application = new Application('f2-api-client', '1.0.0');
10+
$command = new \ItkDev\F2ApiClient\Command\F2ApiClientCommand();
11+
12+
$application->addCommand($command);
13+
14+
$application->setDefaultCommand('f2:api:client', true);
15+
$application->run();

compose.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ services:
99
- PHP_IDE_CONFIG=serverName=localhost
1010
volumes:
1111
- .:/app
12+
# https://docs.docker.com/compose/how-tos/environment-variables/set-environment-variables/#additional-information-1
13+
env_file:
14+
- path: .env.local
15+
required: false
1216

1317
# Code checks tools
1418
markdownlint:

composer.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
"description": "F2 API client",
44
"license": "MIT",
55
"type": "library",
6+
"require": {
7+
"symfony/console": "^6.4 || ^7.4",
8+
"symfony/http-client": "^6.4 || ^7.4",
9+
"symfony/options-resolver": "^6.4 || ^7.4"
10+
},
611
"require-dev": {
12+
"php": "^8.3",
713
"ergebnis/composer-normalize": "^2.52",
814
"friendsofphp/php-cs-fixer": "^3.95"
915
},
@@ -12,6 +18,9 @@
1218
"ItkDev\\F2ApiClient\\": "src/"
1319
}
1420
},
21+
"bin": [
22+
"bin/f2-api-client"
23+
],
1524
"config": {
1625
"allow-plugins": {
1726
"ergebnis/composer-normalize": true

src/ApiClient.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace ItkDev\F2ApiClient;
4+
5+
use Symfony\Component\HttpClient\HttpClient;
6+
use Symfony\Component\OptionsResolver\OptionsResolver;
7+
use Symfony\Contracts\HttpClient\HttpClientInterface;
8+
use Symfony\Contracts\HttpClient\ResponseInterface;
9+
10+
class ApiClient
11+
{
12+
protected const string METHOD_GET = 'GET';
13+
protected const string METHOD_POST = 'POST';
14+
15+
private readonly array $options;
16+
private HttpClientInterface $client;
17+
18+
public function __construct(
19+
array $options,
20+
) {
21+
$resolver = new OptionsResolver();
22+
$this->configureOptions($resolver);
23+
24+
$this->options = $resolver->resolve($options);
25+
}
26+
27+
public function getServiceIndex(): array
28+
{
29+
$path = '/F2Rest/ServiceIndex';
30+
$response = $this->request(self::METHOD_GET, $path);
31+
32+
return $response->toArray();
33+
}
34+
35+
public function searchCases(array $query): array
36+
{
37+
// $query = [];
38+
$path = '/F2Rest/search/cases';
39+
// @todo /F2Rest/ServiceIndex.json reports
40+
//
41+
// "http://cbrain.com/casefile/rel/case-search": {
42+
// "href": "/F2Rest/search/cases",
43+
// "title": "Case file search"
44+
// },
45+
//
46+
// but the actual path is /F2Rest/searches/cases!
47+
$path = '/F2Rest/searches/cases';
48+
$response = $this->request(self::METHOD_GET, $path, [
49+
'query' => $query,
50+
]);
51+
52+
return $response->toArray();
53+
}
54+
55+
/**
56+
* @return array{'access_token': string, 'token_type': string}
57+
*/
58+
protected function getAccessToken(): array
59+
{
60+
// @todo Check existing access token is not expired.
61+
62+
$client = $this->client();
63+
$response = $client->request(self::METHOD_POST, '/F2Rest/oauth2/token', [
64+
'auth_basic' => [
65+
$this->options['api_username'],
66+
$this->options['api_secret'],
67+
],
68+
'headers' => [
69+
'accept' => 'application/json',
70+
],
71+
'body' => [
72+
'grant_type' => 'password',
73+
'username' => $this->options['f2_username'],
74+
],
75+
]);
76+
77+
$token = $response->toArray();
78+
79+
// @todo Store access token.
80+
81+
return $token;
82+
}
83+
84+
protected function request(string $method, string $path, array $options = []): ResponseInterface
85+
{
86+
$accessToken = $this->getAccessToken();
87+
88+
$cmd = 'curl --verbose --header "Authorization: Bearer '.$accessToken['access_token'].'" '.escapeshellarg($this->options['api_uri'].$path.'?'.http_build_query($options['query'] ?? []));
89+
echo PHP_EOL, PHP_EOL, $cmd, PHP_EOL, PHP_EOL;
90+
91+
$cmd .= ' --header "Accept: application/json"';
92+
echo PHP_EOL, PHP_EOL, $cmd, PHP_EOL, PHP_EOL;
93+
94+
return $this->client()->request($method, $path, $options + [
95+
'auth_bearer' => $accessToken['access_token'],
96+
]);
97+
}
98+
99+
protected function client(): HttpClientInterface
100+
{
101+
if (!isset($this->client)) {
102+
$this->client = HttpClient::create([
103+
'base_uri' => $this->options['api_uri'],
104+
]);
105+
}
106+
107+
return $this->client;
108+
}
109+
110+
protected function configureOptions(OptionsResolver $resolver): void
111+
{
112+
$resolver->setRequired([
113+
'api_uri',
114+
'api_username',
115+
'api_secret',
116+
'f2_username',
117+
]);
118+
}
119+
}

src/Command/F2ApiClientCommand.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace ItkDev\F2ApiClient\Command;
4+
5+
use ItkDev\F2ApiClient\ApiClient;
6+
use Symfony\Component\Console\Attribute\Argument;
7+
use Symfony\Component\Console\Attribute\AsCommand;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Exception\InvalidArgumentException;
10+
use Symfony\Component\Console\Exception\RuntimeException;
11+
use Symfony\Component\Console\Style\SymfonyStyle;
12+
13+
#[AsCommand(name: 'f2:api:client')]
14+
class F2ApiClientCommand
15+
{
16+
private ApiClient $client;
17+
18+
public function __invoke(
19+
SymfonyStyle $io,
20+
#[Argument]
21+
string $action,
22+
#[Argument]
23+
?string $arg = null,
24+
): int {
25+
$client = $this->createClient();
26+
if (!method_exists($client, $action)) {
27+
throw new InvalidArgumentException('Invalid action: '.$action);
28+
}
29+
30+
if (null !== $arg) {
31+
$arg = json_decode($arg, true);
32+
}
33+
$response = $client->{$action}($arg);
34+
35+
$io->writeln(json_encode($response, JSON_PRETTY_PRINT));
36+
37+
return Command::SUCCESS;
38+
}
39+
40+
private function createClient(): ApiClient
41+
{
42+
if (!isset($this->client)) {
43+
$getEnv = static function (string $name): string {
44+
$value = getenv($name);
45+
if (false === $value || empty($value)) {
46+
throw new RuntimeException(sprintf('Cannot read environment variable %s', $name));
47+
}
48+
49+
return $value;
50+
};
51+
52+
$config = [
53+
'api_uri' => $getEnv('F2_API_URI'),
54+
'api_username' => $getEnv('F2_API_USERNAME'),
55+
'api_secret' => $getEnv('F2_API_SECRET'),
56+
'f2_username' => $getEnv('F2_F2_USERNAME'),
57+
];
58+
59+
$config = array_filter($config);
60+
if (4 !== count($config)) {
61+
throw new RuntimeException();
62+
}
63+
64+
$this->client = new ApiClient($config);
65+
}
66+
67+
return $this->client;
68+
}
69+
}

0 commit comments

Comments
 (0)