Skip to content

Commit 733b63b

Browse files
committed
Initial Code
1 parent 5e15792 commit 733b63b

Some content is hidden

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

51 files changed

+2452
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/vendor/

CHANGELOG.md

Whitespace-only changes.

CONTRIBUTING.md

Whitespace-only changes.

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Informat API Wrapper
2+
PHP client library for the Informat school software API. This client allows you to
3+
- Fetch students
4+
- Fetch registration
5+
- Create new preregistrations
6+
7+
## Usage
8+
The library is built on 3 main 'layers':
9+
- The `Informat` class, which is instantiated with your client ID and secret.
10+
- Directories, which are logical groups of api calls. These can be obtained by function calls on the `Informat` class.
11+
- API calls, these can be instantiated by function calls on the directories.
12+
13+
The code below is a quick example of how you can fetch students
14+
```php
15+
use Koba\Informat\Informat;
16+
use Koba\Informat\Scopes\AllScopesStrategy;
17+
18+
$informat = new Informat(
19+
'your-client-id',
20+
'your-client-secret',
21+
new AllScopesStrategy('000001', '000002', '000003'),
22+
);
23+
24+
$informat
25+
->students()
26+
->getStudents('000001')
27+
->send();
28+
```
29+
This will return an array of `Student` objects, which has typed (and documented) properties.
30+
31+
### Scopes
32+
Creating the `Informat` object can be done fairly easily as demonstrated above. The scopes paramete might need some extra explanation.
33+
34+
In the example above you see we pass an `AllScopesStrategy` object to the constructor. This is a way to let the API client know which scopes we want to request, which directly impacts which API calls you will be authorized to execute. The library supplies the `AllScopesStrategy`, which will request all possible scopes and the `SpecificScopesStrategy`, which allows you to specify the scopes that will be requested. Both require you to pass the institute numbers for all schools you wnat to access as parameters. The latter might be desirable if your client isn't authorized for all scopes or you want to limit the functionality for security reasons.
35+
36+
You can also create your own scope strategy by implementing the `ScopeStrategyInterface`.
37+
38+
### Calls
39+
There are a couple of 'rules' that should be followed when managing calls
40+
- When creating a call using a function on a directory, all required parameters for the API call need to be passed to the function. The institute number will always be the first parameter as it is required for all API calls.
41+
- All optional paremeters can be set using setters on the API call.
42+
- Setters on the API calls returns the call itself, which means you can easily chain them together.
43+
- An API call will always be executed with the `send` method.
44+
45+
An example to illustrate these points:
46+
```php
47+
$informat
48+
->students()
49+
->getStudent(
50+
'000001', // institute number
51+
'bd26bb65-f54a-488d-866b-e1f9927d6be5' // student id
52+
)
53+
->setReferenceDate(new DateTime())
54+
->send();
55+
```
56+
57+
### Advanced
58+
The `Informat` object has some advanced setup options. You can pass an `AccessTokenManagerInterface` object, which allows you to implement your own access token caching solution. The rest of the constructor can be used to pass a [psr-18](https://www.php-fig.org/psr/psr-18/) client interface and [psr-17](https://www.php-fig.org/psr/psr-17/) HTTP response factories. If these options are omitted, the library will autodiscover any suitable implementations. If your project doesn't currently have one, you could take a look at [Guzzle 7](https://docs.guzzlephp.org/en/stable/).

composer.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "koba/informat-php-client",
3+
"description": "A PHP wrapper for the informat API",
4+
"type": "library",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Wouter Henderickx",
9+
"email": "[email protected]"
10+
}
11+
],
12+
"require": {
13+
"php": "^8.1.0",
14+
"psr/http-client-implementation": "*",
15+
"psr/http-factory-implementation": "*",
16+
"php-http/discovery": "^1.19"
17+
},
18+
"autoload": {
19+
"psr-4": {
20+
"Koba\\Informat\\": "src/Informat/"
21+
}
22+
},
23+
"require-dev": {
24+
"phpstan/phpstan": "^1.10"
25+
},
26+
"config": {
27+
"allow-plugins": {
28+
"php-http/discovery": true
29+
}
30+
}
31+
}

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
parameters:
2+
level: 9
3+
paths:
4+
- src
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Koba\Informat\AccessToken;
4+
5+
class AccessToken
6+
{
7+
protected int $created_at;
8+
9+
public function __construct(
10+
protected string $accessToken,
11+
protected int $expiresIn,
12+
?int $created_at = null
13+
) {
14+
$this->created_at = $created_at ?? time();
15+
}
16+
17+
/**
18+
* Returns whether the access token has expired.
19+
*/
20+
public function expired(): bool
21+
{
22+
return (time() - $this->created_at) > $this->expiresIn;
23+
}
24+
25+
/**
26+
* Returns the actual bearer token you need to include in request headers.
27+
*/
28+
public function getAccessToken(): string
29+
{
30+
return $this->accessToken;
31+
}
32+
33+
/**
34+
* Returns the timestamp on which the access token was created.
35+
*/
36+
public function getCreatedAt(): int
37+
{
38+
return $this->created_at;
39+
}
40+
41+
/**
42+
* Returns the number of seconds after which the access token expires,
43+
* starting from the creation date of the token.
44+
*/
45+
public function getExpiresIn(): int
46+
{
47+
return $this->expiresIn;
48+
}
49+
50+
/**
51+
* String transformation, just returns the bearer token.
52+
* @return string
53+
*/
54+
public function __toString()
55+
{
56+
return $this->getAccessToken();
57+
}
58+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Koba\Informat\AccessToken;
4+
5+
use Koba\Informat\Exceptions\AccessTokenException;
6+
use Koba\Informat\Scopes\ScopeStrategyInterface;
7+
use Psr\Http\Client\ClientInterface;
8+
use Psr\Http\Message\RequestFactoryInterface;
9+
use Psr\Http\Message\StreamFactoryInterface;
10+
11+
class AccessTokenFetcher
12+
{
13+
public function __construct(
14+
protected string $clientId,
15+
protected string $clientSecret,
16+
protected ScopeStrategyInterface $scopeStrategy,
17+
protected ClientInterface $httpClient,
18+
protected RequestFactoryInterface $requestFactory,
19+
protected StreamFactoryInterface $streamFactory
20+
) {
21+
}
22+
23+
public function fetch(): AccessToken
24+
{
25+
$request = $this->requestFactory
26+
->createRequest(
27+
'post',
28+
'https://www.identityserver.be/connect/token'
29+
)
30+
->withAddedHeader('Content-Type', 'application/x-www-form-urlencoded')
31+
->withBody($this->streamFactory->createStream(
32+
http_build_query([
33+
'client_id' => $this->clientId,
34+
'client_secret' => $this->clientSecret,
35+
'scope' => implode(' ', $this->scopeStrategy->getScopes()),
36+
'grant_type' => 'client_credentials'
37+
])
38+
));
39+
40+
$response = $this->httpClient->sendRequest($request);
41+
if ($response->getStatusCode() !== 200) {
42+
throw new AccessTokenException('Fetching access token failed.');
43+
}
44+
45+
$decoded = json_decode(
46+
$response->getBody()->getContents(),
47+
true
48+
);
49+
50+
if (
51+
is_array($decoded)
52+
&& array_key_exists('access_token', $decoded)
53+
&& array_key_exists('expires_in', $decoded)
54+
) {
55+
return new AccessToken($decoded['access_token'], $decoded['expires_in']);
56+
}
57+
58+
throw new AccessTokenException('Invalid access token');
59+
}
60+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Koba\Informat\AccessToken;
4+
5+
class AccessTokenManager implements AccessTokenManagerInterface
6+
{
7+
protected ?AccessToken $accessToken = null;
8+
protected AccessTokenFetcher $accessTokenFetcher;
9+
10+
public function getAccessToken(): AccessToken
11+
{
12+
if ($this->accessToken === null || $this->accessToken->expired()) {
13+
$this->accessToken = $this->accessTokenFetcher->fetch();
14+
}
15+
16+
return $this->accessToken;
17+
}
18+
19+
public function setAccessTokenFetcher(AccessTokenFetcher $fetcher): self
20+
{
21+
$this->accessTokenFetcher = $fetcher;
22+
return $this;
23+
}
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Koba\Informat\AccessToken;
4+
5+
interface AccessTokenManagerInterface
6+
{
7+
/** Should return a valid, non-expired access token */
8+
public function getAccessToken(): AccessToken;
9+
public function setAccessTokenFetcher(AccessTokenFetcher $fetcher): self;
10+
}

0 commit comments

Comments
 (0)