Skip to content

Commit 96a70e5

Browse files
committed
First implementation
1 parent 0829fc5 commit 96a70e5

10 files changed

Lines changed: 1426 additions & 1 deletion

src/CopacoCloudAPIClient.php

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
<?php
2+
3+
namespace Inserve\CopacoCloudAPI;
4+
5+
use Inserve\CopacoCloudAPI\DTO\CustomerList;
6+
use Inserve\CopacoCloudAPI\DTO\SubscriptionList;
7+
use Inserve\CopacoCloudAPI\Exception\CopacoCloudAPIException;
8+
use GuzzleHttp\ClientInterface;
9+
use GuzzleHttp\Exception\GuzzleException;
10+
use Psr\Log\LoggerInterface;
11+
use Psr\Log\NullLogger;
12+
use SensitiveParameter;
13+
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
14+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
15+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
16+
use Symfony\Component\Serializer\Encoder\JsonEncoder;
17+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
18+
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
19+
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
20+
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
21+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
22+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
23+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
24+
use Symfony\Component\Serializer\Serializer;
25+
use Throwable;
26+
27+
/**
28+
*
29+
*/
30+
class CopacoCloudAPIClient
31+
{
32+
protected Serializer $serializer;
33+
34+
protected ObjectNormalizer $normalizer;
35+
36+
private ?string $bearerToken = null;
37+
38+
private array $defaultHeaders = [
39+
'Accept' => 'application/json',
40+
];
41+
42+
/**
43+
* @param ClientInterface $client
44+
* @param LoggerInterface|null $logger
45+
*/
46+
public function __construct(protected ClientInterface $client, protected ?LoggerInterface $logger = null)
47+
{
48+
if (! $this->logger) {
49+
$this->logger = new NullLogger();
50+
}
51+
52+
$this->createNormalizer();
53+
}
54+
55+
/**
56+
* @param string|null $bearerToken
57+
*
58+
* @return $this
59+
*/
60+
public function setBearerToken(#[SensitiveParameter] ?string $bearerToken): self
61+
{
62+
$this->bearerToken = $bearerToken;
63+
64+
return $this;
65+
}
66+
67+
/**
68+
* @param array $query
69+
*
70+
* @return SubscriptionList|null
71+
*/
72+
public function getSubscriptions(array $query = []): ?SubscriptionList
73+
{
74+
/** @var SubscriptionList|null */
75+
return $this->getDto('/api/v2/subscriptions', SubscriptionList::class, $query);
76+
}
77+
78+
/**
79+
* @param array $query
80+
*
81+
* @return CustomerList|null
82+
*/
83+
public function getCustomers(array $query = []): ?CustomerList
84+
{
85+
/** @var CustomerList|null */
86+
return $this->getDto('/api/v2/customers', CustomerList::class, $query);
87+
}
88+
89+
/**
90+
* @template T
91+
* @param string $uri
92+
* @param class-string<T> $dtoClass
93+
* @param array $query
94+
* @param array $headers
95+
*
96+
* @return object|null
97+
* @throws CopacoCloudAPIException
98+
*/
99+
protected function getDto(string $uri, string $dtoClass, array $query = [], array $headers = []): ?object
100+
{
101+
$json = $this->requestRaw('GET', $uri, [
102+
'query' => $query,
103+
'headers' => $headers,
104+
]);
105+
106+
try {
107+
$json = json_decode(
108+
json: $json,
109+
associative: true,
110+
flags: JSON_THROW_ON_ERROR
111+
);
112+
113+
return $this->normalizer->denormalize($json, $dtoClass);
114+
} catch (Throwable $exception) {
115+
$this->logError('Error denormalizing response', [
116+
'uri' => $uri,
117+
'message' => $exception->getMessage(),
118+
]);
119+
}
120+
121+
return null;
122+
}
123+
124+
/**
125+
* @return void
126+
*/
127+
protected function createNormalizer(): void
128+
{
129+
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
130+
$nameConverter = new MetadataAwareNameConverter(
131+
$classMetadataFactory,
132+
new CamelCaseToSnakeCaseNameConverter()
133+
);
134+
135+
$extractor = new PropertyInfoExtractor(
136+
typeExtractors: [
137+
new PhpDocExtractor(),
138+
new ReflectionExtractor(),
139+
]
140+
);
141+
$this->normalizer = new ObjectNormalizer(
142+
classMetadataFactory: $classMetadataFactory,
143+
nameConverter: $nameConverter,
144+
propertyTypeExtractor: $extractor,
145+
defaultContext: [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]
146+
);
147+
148+
$this->serializer = new Serializer(
149+
[$this->normalizer, new ArrayDenormalizer()],
150+
[new JsonEncoder()]
151+
);
152+
}
153+
154+
/**
155+
* @param string $method
156+
* @param string $uri
157+
* @param array $options
158+
*
159+
* @return string
160+
* @throws CopacoCloudAPIException
161+
*/
162+
private function requestRaw(string $method, string $uri, array $options = []): string
163+
{
164+
$headers = array_merge(
165+
$this->defaultHeaders,
166+
$options['headers'] ?? [],
167+
$this->authHeader(),
168+
);
169+
170+
$options['headers'] = $headers;
171+
172+
try {
173+
$response = $this->client->request($method, $uri, $options);
174+
$statusCode = $response->getStatusCode();
175+
176+
if ($statusCode < 200 || $statusCode >= 300) {
177+
$this->logError('API request failed', [
178+
'uri' => $uri,
179+
'method' => $method,
180+
'statusCode' => $statusCode,
181+
]);
182+
183+
throw new CopacoCloudAPIException(
184+
sprintf('API request failed with status: %d', $statusCode),
185+
$statusCode,
186+
);
187+
}
188+
189+
return (string) $response->getBody();
190+
} catch (GuzzleException $e) {
191+
$this->logError('HTTP client exception', [
192+
'uri' => $uri,
193+
'method' => $method,
194+
'code' => $e->getCode(),
195+
'exception' => $e::class,
196+
'message' => $e->getMessage(),
197+
]);
198+
199+
throw new CopacoCloudAPIException(
200+
sprintf('HTTP client error: %s', $e->getMessage()),
201+
$e->getCode() ?: 0
202+
);
203+
}
204+
}
205+
206+
/**
207+
* @return string[]
208+
*/
209+
private function authHeader(): array
210+
{
211+
if ($this->bearerToken !== null) {
212+
return [
213+
'Authorization' => sprintf('Bearer %s', $this->bearerToken),
214+
];
215+
}
216+
217+
return [];
218+
}
219+
220+
/**
221+
* @param string $message
222+
* @param array $context
223+
*
224+
* @return void
225+
*/
226+
private function logError(string $message, array $context = []): void
227+
{
228+
if (! $this->logger) {
229+
return;
230+
}
231+
232+
$this->logger->error($message, $context);
233+
}
234+
}

src/DTO/Customer.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace Inserve\CopacoCloudAPI\DTO;
4+
5+
/**
6+
*
7+
*/
8+
class Customer
9+
{
10+
protected ?string $customerId = null;
11+
protected ?string $customerName = null;
12+
protected ?string $customerStatus = null;
13+
protected ?string $customerCountry = null;
14+
15+
/**
16+
* @return string|null
17+
*/
18+
public function getCustomerId(): ?string
19+
{
20+
return $this->customerId;
21+
}
22+
23+
/**
24+
* @return string|null
25+
*/
26+
public function getCustomerName(): ?string
27+
{
28+
return $this->customerName;
29+
}
30+
31+
/**
32+
* @return string|null
33+
*/
34+
public function getCustomerStatus(): ?string
35+
{
36+
return $this->customerStatus;
37+
}
38+
39+
/**
40+
* @return string|null
41+
*/
42+
public function getCustomerCountry(): ?string
43+
{
44+
return $this->customerCountry;
45+
}
46+
47+
/**
48+
* @param string|null $customerId
49+
*
50+
* @return $this
51+
*/
52+
public function setCustomerId(?string $customerId): self
53+
{
54+
$this->customerId = $customerId;
55+
56+
return $this;
57+
}
58+
59+
/**
60+
* @param string|null $customerName
61+
*
62+
* @return $this
63+
*/
64+
public function setCustomerName(?string $customerName): self
65+
{
66+
$this->customerName = $customerName;
67+
68+
return $this;
69+
}
70+
71+
/**
72+
* @param string|null $customerStatus
73+
*
74+
* @return $this
75+
*/
76+
public function setCustomerStatus(?string $customerStatus): self
77+
{
78+
$this->customerStatus = $customerStatus;
79+
80+
return $this;
81+
}
82+
83+
/**
84+
* @param string|null $customerCountry
85+
*
86+
* @return $this
87+
*/
88+
public function setCustomerCountry(?string $customerCountry): self
89+
{
90+
$this->customerCountry = $customerCountry;
91+
92+
return $this;
93+
}
94+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Inserve\CopacoCloudAPI\DTO;
4+
5+
/**
6+
*
7+
*/
8+
class CustomerCustomProperties
9+
{
10+
protected ?string $customPropertyName = null;
11+
protected ?string $customPropertyValue = null;
12+
13+
/**
14+
* @return string|null
15+
*/
16+
public function getCustomPropertyName(): ?string
17+
{
18+
return $this->customPropertyName;
19+
}
20+
21+
/**
22+
* @return string|null
23+
*/
24+
public function getCustomPropertyValue(): ?string
25+
{
26+
return $this->customPropertyValue;
27+
}
28+
29+
/**
30+
* @param string|null $customPropertyName
31+
*
32+
* @return $this
33+
*/
34+
public function setCustomPropertyName(?string $customPropertyName): self
35+
{
36+
$this->customPropertyName = $customPropertyName;
37+
38+
return $this;
39+
}
40+
41+
/**
42+
* @param string|null $customPropertyValue
43+
*
44+
* @return $this
45+
*/
46+
public function setCustomPropertyValue(?string $customPropertyValue): self
47+
{
48+
$this->customPropertyValue = $customPropertyValue;
49+
50+
return $this;
51+
}
52+
}

0 commit comments

Comments
 (0)