Skip to content

Commit 948c187

Browse files
committed
Initial commit
0 parents  commit 948c187

33 files changed

+1699
-0
lines changed

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build-test-analyse:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
php-version: ['8.2', '8.3', '8.4']
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Setup PHP
20+
uses: shivammathur/setup-php@v2
21+
with:
22+
php-version: ${{ matrix.php-version }}
23+
coverage: none
24+
25+
- name: Install dependencies
26+
run: composer install --prefer-dist --no-interaction --no-progress
27+
28+
- name: Run unit tests
29+
run: composer test
30+
31+
- name: Run code style checks
32+
run: composer cs
33+
34+
- name: Run static analysis
35+
run: composer stan

.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/vendor/
2+
/composer.lock
3+
/.phpunit.result.cache
4+
/.phpunit.cache
5+
/.phpunit/
6+
/.phpcs-cache
7+
/.php_cs.cache
8+
/coverage/
9+
/.idea/
10+
/.vscode/
11+
/phpstan.neon
12+
.phpunit.xml
13+
/.DS_Store
14+
node_modules/
15+
/*.log
16+
/.env
17+
/.env.*
18+
/*.swp
19+
*.sublime-workspace
20+
*.sublime-project
21+
/nbproject/
22+
/build/
23+
/dist/
24+
/tmp/
25+
/tests/_output/

.phpcs.xml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="PHP_CodeSniffer">
3+
<description>The coding standard for our project.</description>
4+
5+
<rule ref="PSR1">
6+
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps" />
7+
</rule>
8+
9+
<rule ref="PSR2"/>
10+
11+
<rule ref="PSR2.Classes.ClassDeclaration"/>
12+
<rule ref="PSR2.Classes.PropertyDeclaration"/>
13+
<rule ref="PSR2.ControlStructures.ControlStructureSpacing"/>
14+
<rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
15+
<rule ref="PSR2.ControlStructures.SwitchDeclaration"/>
16+
<rule ref="PSR2.Files.EndFileNewline"/>
17+
<rule ref="PSR2.Methods.MethodDeclaration"/>
18+
<rule ref="PSR2.Namespaces.NamespaceDeclaration"/>
19+
<rule ref="PSR2.Namespaces.UseDeclaration"/>
20+
21+
<rule ref="PSR12.Operators.OperatorSpacing"/>
22+
23+
<!--http://edorian.github.io/php-coding-standard-generator/#phpcs-->
24+
<rule ref="Generic.Arrays.DisallowLongArraySyntax" />
25+
<rule ref="Generic.PHP.ForbiddenFunctions">
26+
<properties>
27+
<property name="forbiddenFunctions" type="array">
28+
<element key="eval" value="NULL"/>
29+
<element key="dd" value="NULL"/>
30+
<element key="dump" value="NULL"/>
31+
<element key="die" value="NULL"/>
32+
<element key="var_dump" value="NULL"/>
33+
<element key="sizeof" value="count"/>
34+
<element key="delete" value="unset"/>
35+
<element key="print" value="echo"/>
36+
<element key="create_function" value="NULL"/>
37+
</property>
38+
</properties>
39+
</rule>
40+
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
41+
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
42+
<rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
43+
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
44+
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
45+
<rule ref="Generic.Formatting.SpaceAfterCast">
46+
<properties>
47+
<property name="spacing" value="0"/>
48+
</properties>
49+
</rule>
50+
<rule ref="Generic.PHP.DeprecatedFunctions"/>
51+
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
52+
<rule ref="Generic.PHP.ForbiddenFunctions"/>
53+
<rule ref="Generic.PHP.LowerCaseConstant"/>
54+
<rule ref="Generic.Strings.UnnecessaryStringConcat"/>
55+
<rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
56+
<rule ref="Generic.WhiteSpace.ScopeIndent"/>
57+
58+
<rule ref="PEAR.Formatting.MultiLineAssignment"/>
59+
<rule ref="PEAR.WhiteSpace.ObjectOperatorIndent"/>
60+
<rule ref="PEAR.WhiteSpace.ScopeClosingBrace"/>
61+
62+
<exclude-pattern>vendor/*</exclude-pattern>
63+
<exclude-pattern>storage/*</exclude-pattern>
64+
<exclude-pattern>bootstrap/*</exclude-pattern>
65+
<exclude-pattern>public/*</exclude-pattern>
66+
<exclude-pattern>*/migrations/*</exclude-pattern>
67+
<exclude-pattern>*/seeds/*</exclude-pattern>
68+
<exclude-pattern>*.blade.php</exclude-pattern>
69+
<exclude-pattern>*.js</exclude-pattern>
70+
71+
<file>src</file>
72+
<file>tests</file>
73+
74+
<!-- Show progression -->
75+
<arg value="p"/>
76+
<arg value="n"/>
77+
<arg value="s"/>
78+
</ruleset>

composer.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "wedesignit/common",
3+
"description": "Reusable PSR-compatible API client framework with middleware and token authentication",
4+
"type": "library",
5+
"license": "MIT",
6+
"require": {
7+
"php": "^8.2 || ^8.3 || ^8.4",
8+
"psr/http-client": "^1.0",
9+
"psr/http-message": "^1.0",
10+
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
11+
"psr/log": "^1.1 || ^2.0 || ^3.0"
12+
},
13+
"require-dev": {
14+
"phpunit/phpunit": "^10.0",
15+
"squizlabs/php_codesniffer": "^3.7",
16+
"phpstan/phpstan": "^1.10",
17+
"guzzlehttp/psr7": "^2.0",
18+
"symfony/cache": "^6.0 || ^7.0"
19+
},
20+
"autoload": {
21+
"psr-4": {
22+
"WeDesignIt\\Common\\": "src/"
23+
}
24+
},
25+
"autoload-dev": {
26+
"psr-4": {
27+
"WeDesignIt\\Common\\Tests\\": "tests/"
28+
}
29+
},
30+
"scripts": {
31+
"test": "phpunit",
32+
"cs": "phpcs --standard=PSR12 src/ tests/",
33+
"stan": "phpstan analyse src/ --level=max"
34+
},
35+
"minimum-stability": "stable",
36+
"prefer-stable": true
37+
}

docs/examples.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Examples
2+
3+
## Building ApiClients
4+
5+
Example code for building an ApiClient with bearer token authentication:
6+
```php
7+
namespace App\Api;
8+
9+
use WeDesignIt\Common\Api\ApiClient;
10+
use WeDesignIt\Common\Api\Traits\UsesBearerToken;
11+
12+
class ExampleApiClient extends ApiClient
13+
{
14+
use UsesBearerToken;
15+
16+
public function __construct(string $baseUri, string $bearerToken)
17+
{
18+
parent::__construct($baseUri);
19+
$this->setBearerToken($bearerToken);
20+
}
21+
22+
/**
23+
* Simple example call to get the current user.
24+
*/
25+
public function getCurrentUser(): array
26+
{
27+
return $this->request('GET', '/me');
28+
}
29+
}
30+
```
31+
32+
Example code for building an ApiClient using an identity provider (idp). Note that the `UsesTokenCaching` trait is used to
33+
cache the token for subsequent requests, and the `UsesIdpToken` trait handles the IDP-specific token fetching logic.
34+
The `UsesTokenCaching` trait is optional but recommended to avoid unnecessary token requests.
35+
36+
```php
37+
namespace App\Api;
38+
39+
use App\Api\ApiClient;
40+
use App\Api\Traits\UsesIdpToken;
41+
use App\Api\Traits\UsesTokenCaching;
42+
use Psr\SimpleCache\CacheInterface;
43+
44+
class IdpApiClient extends ApiClient
45+
{
46+
use UsesIdpToken, UsesTokenCaching;
47+
48+
protected string $clientId;
49+
protected string $clientSecret;
50+
51+
public function __construct(
52+
string $baseUri,
53+
string $clientId,
54+
string $clientSecret,
55+
?CacheInterface $tokenCache = null,
56+
?ClientInterface $httpClient = null
57+
) {
58+
parent::__construct($baseUri, $httpClient);
59+
$this->clientId = $clientId;
60+
$this->clientSecret = $clientSecret;
61+
if ($tokenCache) {
62+
$this->setTokenCache($tokenCache);
63+
}
64+
}
65+
66+
/**
67+
* Generates unique cache key for the IDP token.
68+
*/
69+
protected function getTokenCacheKey(): string
70+
{
71+
return 'api:idp_token:' . sha1($this->baseUri . ':' . $this->clientId);
72+
}
73+
74+
protected function fetchTokenFromIdp(): array
75+
{
76+
// note things like grant_type, client_id, client_secret, scope, etc. are specific to the IDP you are using.
77+
$response = $this->httpClient->sendRequest(
78+
$this->requestFactory->createRequest('POST', 'https://idp.example.com/oauth/token')
79+
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
80+
->withBody(
81+
$this->streamFactory->createStream(http_build_query([
82+
'grant_type' => 'client_credentials',
83+
'client_id' => $this->clientId,
84+
'client_secret' => $this->clientSecret,
85+
'scope' => 'api.read', // of de benodigde scopes
86+
]))
87+
)
88+
);
89+
90+
$data = json_decode((string) $response->getBody(), true);
91+
92+
if (!isset($data['access_token'], $data['expires_in'])) {
93+
throw new \RuntimeException('Ongeldige IDP token response');
94+
}
95+
96+
// If a refresh token is provided, include it in the response
97+
return [
98+
'access_token' => $data['access_token'],
99+
'expires_in' => $data['expires_in'],
100+
'refresh_token' => $data['refresh_token'] ?? null,
101+
];
102+
}
103+
104+
/**
105+
* Simple API call to get a "protected resource".
106+
*/
107+
public function getProtectedResource(): array
108+
{
109+
return $this->request('GET', '/resource');
110+
}
111+
}
112+
```
113+
114+
Stacking multiple middlewares in an API client. In this case the above `IdpApiClient` can be used with the `StackClient` to add additional middlewares like logging or caching.:
115+
116+
```php
117+
// Get dependencies from the service container:
118+
$logger = app(LoggerInterface::class);
119+
$cache = Cache::store('redis');
120+
121+
// Stack middlewares to be used with the API client
122+
$middleware = [
123+
new LogMiddleware($logger), // Log requests and responses
124+
new CacheMiddleware($cache, ttl: 300), // Cache GET responses for 5 minutes
125+
new RequestThrottlingMiddleware( // Throttle the client to 100 requests per minute, per API client
126+
$cache, 100, 60
127+
),
128+
new RetryMiddleware( // Retry on 429, 500, 503 (max 3 times with exponential backoff)
129+
maxAttempts: 3,
130+
baseDelayMs: 300,
131+
retryOnStatus: [429, 500, 502, 503, 504],
132+
retryOnException: [\RuntimeException::class]
133+
),
134+
];
135+
136+
// Use the StackClient and stuff in the middlewares
137+
$httpClient = new StackClient($middleware);
138+
139+
// And let the API client use the stack client
140+
$api = new IdpApiClient(
141+
'https://api.example.com',
142+
'your-client-id',
143+
'your-client-secret',
144+
$cache, // Token cache
145+
$httpClient // PSR-18 client met middleware stack
146+
);
147+
148+
// Now use the API client to make requests
149+
$data = $api->getProtectedResource();
150+
```

0 commit comments

Comments
 (0)