Skip to content

Commit

Permalink
Mutualize services
Browse files Browse the repository at this point in the history
  • Loading branch information
odolbeau committed Apr 4, 2024
1 parent 8f3ae7c commit 242e90a
Show file tree
Hide file tree
Showing 17 changed files with 688 additions and 250 deletions.
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,30 @@ composer require doskyft/helloasso-php

## Utilisation

Pour commencer, il faut créer une instance de `HelloassoClient`.

```php
$helloasso = new HelloassoClient(
use Helloasso\HelloassoClientFactory;

$helloassoClient = HelloassoClientFactory::create(
'hello_asso_id',
'hello_asso_secret',
'hello_asso_organization_slug',
true # sandbox
);
```

Maintenant, on peut commencer à utiliser le client

### CheckoutIntent

<details>

<summary>Créer un CheckoutIntent</summary>

```php
use Helloasso\Models\Carts\CheckoutPayer;
use Helloasso\Models\Carts\InitCheckoutBody;

$checkoutIntent = (new InitCheckoutBody())
->setTotalAmount(1000)
Expand All @@ -35,8 +52,23 @@ $checkoutIntent = (new InitCheckoutBody())
])
;

$helloasso->checkout->create($checkoutIntent);
$helloassoClient->checkout->create($checkoutIntent);
```
[Voir la documentation](https://api.helloasso.com/v5/swagger/ui/index#/Checkout%20intents%20management/OrganizationCheckoutIntents_PostInitCheckout)
</details>

### Évènements

<details>

<summary></summary>

```php
use Helloasso\Models\Event;

$event = $helloassoClient->decodeEvent($rawEventReceivedFromHelloasso); // Returns an instance of Event
```
</details>

## Contributeurs

Expand Down Expand Up @@ -67,4 +99,4 @@ $helloasso->checkout->create($checkoutIntent);
</a>
</td>
</tr>
</table>
</table>
63 changes: 51 additions & 12 deletions src/HelloassoClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,65 @@

namespace Helloasso;

use Helloasso\Exception\InvalidValueException;
use Helloasso\Http\ApiCaller;
use Helloasso\Models\Event;
use Helloasso\Models\Forms\FormPublicModel;
use Helloasso\Models\Statistics\OrderDetail;
use Helloasso\Models\Statistics\PaymentDetail;
use Helloasso\Service\CheckoutIntentService;
use Helloasso\Service\DirectoryService;
use Helloasso\Service\EventService;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;

class HelloassoClient
readonly class HelloassoClient
{
public CheckoutIntentService $checkout;

public DirectoryService $directory;

public EventService $event;

public function __construct(
readonly string $clientId,
readonly string $clientSecret,
readonly string $organizationSlug,
readonly bool $sandbox = false,
private SerializerInterface|DenormalizerInterface $serializer,
ApiCaller $apiCaller,
string $organizationSlug,
) {
$this->checkout = new CheckoutIntentService($clientId, $clientSecret, $organizationSlug, $sandbox);
$this->directory = new DirectoryService($clientId, $clientSecret, $organizationSlug, $sandbox);
$this->event = new EventService($clientId, $clientSecret, $organizationSlug, $sandbox);
$this->checkout = new CheckoutIntentService($apiCaller, $organizationSlug);
$this->directory = new DirectoryService();
}

/**
* Decode an Helloasso event content (webhook).
*
* @throws InvalidValueException
*/
public function decodeEvent(string $eventData): Event
{
try {
$event = json_decode($eventData, true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new InvalidValueException('Unable to json_decode given event.');
}

$eventType = $event['eventType'] ?? null;
if (null === $eventType) {
throw new InvalidValueException('No eventType key found in given event.');
}

$data = $event['data'] ?? null;
if (null === $data) {
throw new InvalidValueException('No data key found in given event.');
}

$expectedModel = match ($eventType) {
Event::EVENT_TYPE_FORM => FormPublicModel::class,
Event::EVENT_TYPE_ORDER => OrderDetail::class,
Event::EVENT_TYPE_PAYMENT => PaymentDetail::class,
default => throw new InvalidValueException('eventType "'.$eventType.'" not supported')
};

return (new Event())
->setMetadata($event['metadata'] ?? [])
->setData($this->serializer->denormalize($data, $expectedModel))
->setEventType($eventType)
;
}
}
59 changes: 59 additions & 0 deletions src/HelloassoClientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Helloasso;

use Helloasso\Http\ApiCaller;
use Helloasso\Http\ResponseHandler;
use Helloasso\Http\TokenManager;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

class HelloassoClientFactory
{
public static function create(
string $clientId,
string $clientSecret,
string $organizationSlug,
bool $sandbox = false,
): HelloassoClient {
$httpClient = HttpClient::createForBaseUri($sandbox ? 'https://api.helloasso-sandbox.com' : 'https://api.helloasso.com', [
'headers' => [
'accept' => 'application/json',
'Content-type: application/json',
],
]);
$serializer = self::createSerializer();
$responseHandler = new ResponseHandler($serializer);
$tokenManager = new TokenManager($httpClient, $responseHandler, $clientId, $clientSecret);

return new HelloassoClient(
$serializer,
new ApiCaller($httpClient, $tokenManager, $responseHandler, $serializer),
$organizationSlug,
);
}

private static function createSerializer(): Serializer
{
$encoder = [new JsonEncoder()];
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = [
new DateTimeNormalizer(),
new BackedEnumNormalizer(),
new ArrayDenormalizer(),
new ObjectNormalizer(null, null, null, $extractor),
];

return new Serializer($normalizer, $encoder);
}
}
54 changes: 54 additions & 0 deletions src/Http/ApiCaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Helloasso\Http;

use Helloasso\Models\HelloassoObject;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Serializer;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class ApiCaller
{
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly TokenManager $tokenManager,
private readonly ResponseHandler $responseHandler,
private readonly Serializer $serializer,
) {
}

/**
* @template T of HelloassoObject
*
* @param class-string<T> $responseClassType
*
* @return T
*/
public function post(string $url, array|HelloassoObject|null $body, string $responseClassType): HelloassoObject
{
$response = $this->httpClient->request(Request::METHOD_POST, $url, [
'auth_bearer' => $this->tokenManager->getAccessToken(),
'body' => $this->serializer->serialize($body, 'json'),
]);

return $this->responseHandler->deserializeResponse($response, $responseClassType);
}

/**
* @template T of HelloassoObject
*
* @param class-string<T> $responseClassType
*
* @return T
*/
public function get(string $url, string $responseClassType): HelloassoObject
{
$response = $this->httpClient->request(Request::METHOD_GET, $url, [
'auth_bearer' => $this->tokenManager->getAccessToken(),
]);

return $this->responseHandler->deserializeResponse($response, $responseClassType);
}
}
58 changes: 58 additions & 0 deletions src/Http/ResponseHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Helloasso\Http;

use Helloasso\Exception\HelloassoApiException;
use Helloasso\Models\HelloassoObject;
use Symfony\Component\Serializer\Serializer;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

class ResponseHandler
{
public function __construct(
private readonly Serializer $serializer,
) {
}

/**
* @template T of HelloassoObject
*
* @param class-string<T> $responseClassType
*
* @return T
*
* @throws HelloassoApiException
*/
public function deserializeResponse(ResponseInterface $response, string $responseClassType): HelloassoObject
{
try {
$responseContent = $response->getContent();
} catch (HttpExceptionInterface|TransportExceptionInterface $e) {
try {
$content = $response->getContent(false);
} catch (HttpExceptionInterface|TransportExceptionInterface $e) {
$content = 'unknown error';
}

throw new HelloassoApiException($e->getMessage().' : '.$content);
}

return $this->deserializeResponseContent($responseContent, $responseClassType);
}

/**
* @template T of HelloassoObject
*
* @param class-string<T> $responseClassType
*
* @return T
*/
public function deserializeResponseContent(string $content, string $responseClassType): HelloassoObject
{
return $this->serializer->deserialize($content, $responseClassType, 'json');
}
}
49 changes: 49 additions & 0 deletions src/Http/TokenManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Helloasso\Http;

use Helloasso\Models\ClientCredentials;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class TokenManager
{
private ?string $accessToken = null;

public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly ResponseHandler $responseHandler,
private readonly string $clientId,
private readonly string $clientSecret,
) {
}

public function getAccessToken(): string
{
if (null === $this->accessToken) {
$this->retrieveAccessToken();
}

return $this->accessToken;
}

private function retrieveAccessToken(): void
{
$response = $this->httpClient->request(Request::METHOD_POST, '/oauth2/token', [
'body' => [
'grant_type' => 'client_credentials',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
],
'headers' => [
'Content-Type: application/x-www-form-urlencoded',
],
]);

$credentials = $this->responseHandler->deserializeResponse($response, ClientCredentials::class);

$this->accessToken = $credentials->getAccessToken();
}
}
4 changes: 2 additions & 2 deletions src/Models/Forms/TierPublicModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ public function getVatRate(): float
return $this->vatRate;
}

public function setVatRate(float $vatRate): self
public function setVatRate(float|int $vatRate): self
{
$this->vatRate = $vatRate;
$this->vatRate = (float) $vatRate;

return $this;
}
Expand Down
Loading

0 comments on commit 242e90a

Please sign in to comment.