Skip to content

Commit 2336ce2

Browse files
authored
Raise PHPStan level to max and allow multiple mailers (#23)
* Work towards PHPStan level max * Handle invalid configuration * Fix tests on older pest versions
1 parent 0c0e892 commit 2336ce2

9 files changed

+183
-87
lines changed

.github/workflows/phpstan.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ jobs:
1111
name: phpstan
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v4
14+
- name: Checkout code
15+
uses: actions/checkout@v4
1516

1617
- name: Setup PHP
1718
uses: shivammathur/setup-php@v2
@@ -23,4 +24,4 @@ jobs:
2324
uses: ramsey/composer-install@v3
2425

2526
- name: Run PHPStan
26-
run: ./vendor/bin/phpstan --error-format=github
27+
run: vendor/bin/phpstan --error-format=github

phpstan.neon.dist

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ includes:
22
- phpstan-baseline.neon
33

44
parameters:
5-
level: 8
5+
level: max
66
paths:
77
- src
88
tmpDir: build/phpstan
99
checkOctaneCompatibility: true
1010
checkModelProperties: true
1111
checkMissingIterableValueType: false
1212
checkGenericClassInNonGenericObjectType: false
13-
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace InnoGE\LaravelMsGraphMail\Exceptions;
4+
5+
use Exception;
6+
7+
class ConfigurationInvalid extends Exception
8+
{
9+
public function __construct(string $key, mixed $value)
10+
{
11+
$invalidValue = var_export($value, true);
12+
parent::__construct("Configuration key {$key} for microsoft-graph mailer has invalid value: {$invalidValue}.");
13+
}
14+
}

src/Exceptions/ConfigurationMissing.php

+2-17
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,8 @@
66

77
class ConfigurationMissing extends Exception
88
{
9-
public static function tenantId(): self
9+
public function __construct(string $key)
1010
{
11-
return new self('The tenant id is missing from the configuration file.');
12-
}
13-
14-
public static function clientId(): self
15-
{
16-
return new self('The client id is missing from the configuration file.');
17-
}
18-
19-
public static function clientSecret(): self
20-
{
21-
return new self('The client secret is missing from the configuration file.');
22-
}
23-
24-
public static function fromAddress(): self
25-
{
26-
return new self('The mail from address is missing from the configuration file.');
11+
parent::__construct("Configuration key {$key} for microsoft-graph mailer is missing.");
2712
}
2813
}

src/Exceptions/InvalidResponse.php

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace InnoGE\LaravelMsGraphMail\Exceptions;
4+
5+
use Exception;
6+
7+
class InvalidResponse extends Exception
8+
{
9+
}

src/LaravelMsGraphMailServiceProvider.php

+31-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace InnoGE\LaravelMsGraphMail;
44

55
use Illuminate\Support\Facades\Mail;
6+
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationInvalid;
67
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationMissing;
78
use InnoGE\LaravelMsGraphMail\Services\MicrosoftGraphApiService;
89
use Spatie\LaravelPackageTools\Package;
@@ -23,26 +24,40 @@ public function configurePackage(Package $package): void
2324

2425
public function boot(): void
2526
{
26-
$this->app->bind(MicrosoftGraphApiService::class, function () {
27-
//throw exceptions when config is missing
28-
throw_unless(filled(config('mail.mailers.microsoft-graph.tenant_id')), ConfigurationMissing::tenantId());
29-
throw_unless(filled(config('mail.mailers.microsoft-graph.client_id')), ConfigurationMissing::clientId());
30-
throw_unless(filled(config('mail.mailers.microsoft-graph.client_secret')), ConfigurationMissing::clientSecret());
31-
32-
return new MicrosoftGraphApiService(
33-
tenantId: config('mail.mailers.microsoft-graph.tenant_id', ''),
34-
clientId: config('mail.mailers.microsoft-graph.client_id', ''),
35-
clientSecret: config('mail.mailers.microsoft-graph.client_secret', ''),
36-
accessTokenTtl: config('mail.mailers.microsoft-graph.access_token_ttl', 3000),
37-
);
38-
});
27+
Mail::extend('microsoft-graph', function (array $config): MicrosoftGraphTransport {
28+
throw_if(blank($config['from']['address'] ?? []), new ConfigurationMissing('from.address'));
3929

40-
Mail::extend('microsoft-graph', function (array $config) {
41-
throw_unless(filled($config['from']['address'] ?? []), ConfigurationMissing::fromAddress());
30+
$accessTokenTtl = $config['access_token_ttl'] ?? 3000;
31+
if (! is_int($accessTokenTtl)) {
32+
throw new ConfigurationInvalid('access_token_ttl', $accessTokenTtl);
33+
}
4234

4335
return new MicrosoftGraphTransport(
44-
$this->app->make(MicrosoftGraphApiService::class)
36+
new MicrosoftGraphApiService(
37+
tenantId: $this->requireConfigString($config, 'tenant_id'),
38+
clientId: $this->requireConfigString($config, 'client_id'),
39+
clientSecret: $this->requireConfigString($config, 'client_secret'),
40+
accessTokenTtl: $accessTokenTtl,
41+
),
4542
);
4643
});
4744
}
45+
46+
/**
47+
* @param array<string, mixed> $config
48+
* @return non-empty-string
49+
*/
50+
protected function requireConfigString(array $config, string $key): string
51+
{
52+
if (! array_key_exists($key, $config)) {
53+
throw new ConfigurationMissing($key);
54+
}
55+
56+
$value = $config[$key];
57+
if (! is_string($value) || $value === '') {
58+
throw new ConfigurationInvalid($key, $value);
59+
}
60+
61+
return $value;
62+
}
4863
}

src/MicrosoftGraphTransport.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ protected function prepareAttachments(Email $email, ?string $html): array
8282
}
8383

8484
/**
85-
* @param Collection<Address> $recipients
85+
* @param Collection<array-key, Address> $recipients
8686
*/
8787
protected function transformEmailAddresses(Collection $recipients): array
8888
{
@@ -101,7 +101,7 @@ protected function transformEmailAddress(Address $address): array
101101
}
102102

103103
/**
104-
* @return Collection<Address>
104+
* @return Collection<array-key, Address>
105105
*/
106106
protected function getRecipients(Email $email, Envelope $envelope): Collection
107107
{

src/Services/MicrosoftGraphApiService.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Http\Client\Response;
88
use Illuminate\Support\Facades\Cache;
99
use Illuminate\Support\Facades\Http;
10+
use InnoGE\LaravelMsGraphMail\Exceptions\InvalidResponse;
1011

1112
class MicrosoftGraphApiService
1213
{
@@ -48,7 +49,13 @@ protected function getAccessToken(): string
4849

4950
$response->throw();
5051

51-
return $response->json('access_token');
52+
$accessToken = $response->json('access_token');
53+
if (! is_string($accessToken)) {
54+
$notString = var_export($accessToken, true);
55+
throw new InvalidResponse("Expected response to contain key access_token of type string, got: {$notString}.");
56+
}
57+
58+
return $accessToken;
5259
});
5360
}
5461
}

tests/MicrosoftGraphTransportTest.php

+113-47
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
use Illuminate\Support\Facades\Http;
77
use Illuminate\Support\Facades\Mail;
88
use Illuminate\Support\Str;
9+
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationInvalid;
910
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationMissing;
11+
use InnoGE\LaravelMsGraphMail\Exceptions\InvalidResponse;
1012
use InnoGE\LaravelMsGraphMail\Tests\Stubs\TestMail;
1113
use InnoGE\LaravelMsGraphMail\Tests\Stubs\TestMailWithInlineImage;
1214

@@ -219,69 +221,133 @@
219221
->toBe('foo_access_token');
220222
});
221223

222-
it('throws exceptions when config is missing', function (array $config, string $exceptionMessage) {
224+
it('throws exceptions on invalid access token in response', function () {
225+
Config::set('mail.mailers.microsoft-graph', [
226+
'transport' => 'microsoft-graph',
227+
'client_id' => 'foo_client_id',
228+
'client_secret' => 'foo_client_secret',
229+
'tenant_id' => 'foo_tenant_id',
230+
'from' => [
231+
'address' => '[email protected]',
232+
'name' => 'Taylor Otwell',
233+
],
234+
]);
235+
Config::set('mail.default', 'microsoft-graph');
236+
237+
Http::fake([
238+
'https://login.microsoftonline.com/foo_tenant_id/oauth2/v2.0/token' => Http::response(['access_token' => 123]),
239+
]);
240+
241+
expect(fn () => Mail::to('[email protected]')->send(new TestMail(false)))
242+
->toThrow(InvalidResponse::class, 'Expected response to contain key access_token of type string, got: 123.');
243+
});
244+
245+
it('throws exceptions when config is invalid', function (array $config, Exception $exception) {
223246
Config::set('mail.mailers.microsoft-graph', $config);
224247
Config::set('mail.default', 'microsoft-graph');
225248

226-
try {
227-
Mail::to('[email protected]')
228-
->send(new TestMail(false));
229-
} catch (Exception $e) {
230-
expect($e)
231-
->toBeInstanceOf(ConfigurationMissing::class)
232-
->getMessage()->toBe($exceptionMessage);
233-
}
234-
})->with(
249+
expect(fn () => Mail::to('[email protected]')->send(new TestMail(false)))
250+
->toThrow(get_class($exception), $exception->getMessage());
251+
})->with([
235252
[
236253
[
237-
[
238-
'transport' => 'microsoft-graph',
239-
'client_id' => 'foo_client_id',
240-
'client_secret' => 'foo_client_secret',
241-
'tenant_id' => '',
242-
'from' => [
243-
'address' => '[email protected]',
244-
'name' => 'Taylor Otwell',
245-
],
254+
'transport' => 'microsoft-graph',
255+
'client_id' => 'foo_client_id',
256+
'client_secret' => 'foo_client_secret',
257+
'from' => [
258+
'address' => '[email protected]',
259+
'name' => 'Taylor Otwell',
246260
],
247-
'The tenant id is missing from the configuration file.',
248261
],
262+
new ConfigurationMissing('tenant_id'),
263+
],
264+
[
249265
[
250-
[
251-
'transport' => 'microsoft-graph',
252-
'client_id' => '',
253-
'client_secret' => 'foo_client_secret',
254-
'tenant_id' => 'foo_tenant_id',
255-
'from' => [
256-
'address' => '[email protected]',
257-
'name' => 'Taylor Otwell',
258-
],
266+
'transport' => 'microsoft-graph',
267+
'tenant_id' => 123,
268+
'client_id' => 'foo_client_id',
269+
'client_secret' => 'foo_client_secret',
270+
'from' => [
271+
'address' => '[email protected]',
272+
'name' => 'Taylor Otwell',
259273
],
260-
'The client id is missing from the configuration file.',
261274
],
275+
new ConfigurationInvalid('tenant_id', 123),
276+
],
277+
[
262278
[
263-
[
264-
'transport' => 'microsoft-graph',
265-
'client_id' => 'foo_client_id',
266-
'client_secret' => '',
267-
'tenant_id' => 'foo_tenant_id',
268-
'from' => [
269-
'address' => '[email protected]',
270-
'name' => 'Taylor Otwell',
271-
],
279+
'transport' => 'microsoft-graph',
280+
'tenant_id' => 'foo_tenant_id',
281+
'client_secret' => 'foo_client_secret',
282+
'from' => [
283+
'address' => '[email protected]',
284+
'name' => 'Taylor Otwell',
272285
],
273-
'The client secret is missing from the configuration file.',
274286
],
287+
new ConfigurationMissing('client_id'),
288+
],
289+
[
275290
[
276-
[
277-
'transport' => 'microsoft-graph',
278-
'client_id' => 'foo_client_id',
279-
'client_secret' => 'foo_client_secret',
280-
'tenant_id' => 'foo_tenant_id',
291+
'transport' => 'microsoft-graph',
292+
'tenant_id' => 'foo_tenant_id',
293+
'client_id' => '',
294+
'client_secret' => 'foo_client_secret',
295+
'from' => [
296+
'address' => '[email protected]',
297+
'name' => 'Taylor Otwell',
281298
],
282-
'The mail from address is missing from the configuration file.',
283299
],
284-
]);
300+
new ConfigurationInvalid('client_id', ''),
301+
],
302+
[
303+
[
304+
'transport' => 'microsoft-graph',
305+
'tenant_id' => 'foo_tenant_id',
306+
'client_id' => 'foo_client_id',
307+
'from' => [
308+
'address' => '[email protected]',
309+
'name' => 'Taylor Otwell',
310+
],
311+
],
312+
new ConfigurationMissing('client_secret'),
313+
],
314+
[
315+
[
316+
'transport' => 'microsoft-graph',
317+
'tenant_id' => 'foo_tenant_id',
318+
'client_id' => 'foo_client_id',
319+
'client_secret' => null,
320+
'from' => [
321+
'address' => '[email protected]',
322+
'name' => 'Taylor Otwell',
323+
],
324+
],
325+
new ConfigurationInvalid('client_secret', null),
326+
],
327+
[
328+
[
329+
'transport' => 'microsoft-graph',
330+
'tenant_id' => 'foo_tenant_id',
331+
'client_id' => 'foo_client_id',
332+
'client_secret' => 'foo_client_secret',
333+
],
334+
new ConfigurationMissing('from.address'),
335+
],
336+
[
337+
[
338+
'transport' => 'microsoft-graph',
339+
'tenant_id' => 'foo_tenant_id',
340+
'client_id' => 'foo_client_id',
341+
'client_secret' => 'foo_client_secret',
342+
'access_token_ttl' => false,
343+
'from' => [
344+
'address' => '[email protected]',
345+
'name' => 'Taylor Otwell',
346+
],
347+
],
348+
new ConfigurationInvalid('access_token_ttl', false),
349+
],
350+
]);
285351

286352
it('sends html mails with inline images with microsoft graph', function () {
287353
Config::set('mail.mailers.microsoft-graph', [

0 commit comments

Comments
 (0)