Skip to content

Commit 1215fd0

Browse files
committed
refactor: remove support for tagged mailers
1 parent bc005a4 commit 1215fd0

27 files changed

+235
-276
lines changed

packages/mailer/src/Attachments/DataAttachment.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
use Closure;
66

77
/**
8-
* Represents an attachment that leaves in the local filesystem.
8+
* Represents an attachment that is resolved through a closure.
99
*/
1010
final readonly class DataAttachment implements Attachment
1111
{
1212
private function __construct(
13-
public Closure $resolve,
14-
public ?string $name,
15-
public ?string $contentType,
13+
public readonly Closure $resolve,
14+
public readonly ?string $name,
15+
public readonly ?string $contentType,
1616
) {}
1717

1818
/**

packages/mailer/src/Attachments/StorageAttachment.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Closure;
66
use Tempest\Container\GenericContainer;
7+
use Tempest\Mail\Exceptions\FileAttachmentWasNotFound;
78
use Tempest\Storage\Storage;
89
use Tempest\Support\Path;
910
use UnitEnum;
@@ -30,7 +31,11 @@ private function __construct(
3031
public static function fromPath(string $path, ?string $name = null, ?string $contentType = null, null|string|UnitEnum $tag = null): self
3132
{
3233
if (! ($storage = self::resolveStorage($tag))) {
33-
throw new \RuntimeException('No storage found.');
34+
throw FileAttachmentWasNotFound::storageDoesNotExist($tag);
35+
}
36+
37+
if (! $storage->fileOrDirectoryExists($path)) {
38+
throw FileAttachmentWasNotFound::forStorageFile($path, $tag);
3439
}
3540

3641
$path = Path\normalize($path);

packages/mailer/src/Builder/EmailBuilder.php renamed to packages/mailer/src/Builder/Email.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Tempest\Mail\Attachments\FileAttachment;
99
use Tempest\Mail\Attachments\StorageAttachment;
1010
use Tempest\Mail\Content;
11-
use Tempest\Mail\Email;
11+
use Tempest\Mail\Email as EmailInterface;
1212
use Tempest\Mail\Envelope;
1313
use Tempest\Mail\GenericEmail;
1414
use Tempest\Mail\Priority;
@@ -17,7 +17,7 @@
1717
use Tempest\View\View;
1818
use UnitEnum;
1919

20-
final class EmailBuilder
20+
final class Email
2121
{
2222
public function __construct(
2323
private(set) null|string|array|ArrayInterface|Address $to = null,
@@ -184,7 +184,7 @@ public function withStorageAttachment(string $path, ?string $name = null, ?strin
184184
/**
185185
* Builds the email.
186186
*/
187-
public function make(): Email
187+
public function make(): EmailInterface
188188
{
189189
return new GenericEmail(
190190
envelope: new Envelope(

packages/mailer/src/EmailToSymfonyEmailMapper.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use Symfony\Component\Mime\Address as SymfonyAddress;
88
use Symfony\Component\Mime\Email as SymfonyEmail;
9-
use Symfony\Component\Mime\Header\Headers;
109
use Tempest\Mail\Exceptions\ExpeditorWasMissing;
1110
use Tempest\Mail\Exceptions\RecipientWasMissing;
1211
use Tempest\Mapper\Mapper;
@@ -95,13 +94,11 @@ public function map(mixed $from, mixed $to): SymfonyEmail
9594
private function convertAddresses(null|string|array|Address $addresses): array
9695
{
9796
return arr($addresses)
98-
->map(function (string|Address|SymfonyAddress $address) {
99-
return match (true) {
100-
$address instanceof SymfonyAddress => $address,
101-
$address instanceof Address => new SymfonyAddress($address->email, $address->name),
102-
is_string($address) => SymfonyAddress::create($address),
103-
default => null,
104-
};
97+
->map(fn (string|Address|SymfonyAddress $address) => match (true) {
98+
$address instanceof SymfonyAddress => $address,
99+
$address instanceof Address => new SymfonyAddress($address->email, $address->name),
100+
is_string($address) => SymfonyAddress::create($address),
101+
default => null,
105102
})
106103
->filter()
107104
->toArray();

packages/mailer/src/Exceptions/FileAttachmentWasNotFound.php

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,45 @@
33
namespace Tempest\Mail\Exceptions;
44

55
use Exception;
6+
use UnitEnum;
67

78
final class FileAttachmentWasNotFound extends Exception implements MailerException
89
{
9-
public function __construct(
10-
public readonly string $file,
11-
) {
12-
parent::__construct("File {$file} could not be found on the filesystem.");
10+
public static function forFilesystemFile(string $attachment): self
11+
{
12+
return new self("File {$attachment} could not be found on the filesystem.");
13+
}
14+
15+
public static function forStorageFile(string $attachment, null|string|UnitEnum $tag): self
16+
{
17+
$name = static::resolveStorageTag($tag);
18+
19+
if (is_null($name)) {
20+
return new self("File {$attachment} could not be found in the storage.");
21+
}
22+
23+
return new self("File {$attachment} could not be found in the storage {$name}.");
24+
}
25+
26+
public static function storageDoesNotExist(null|string|UnitEnum $tag): self
27+
{
28+
$name = static::resolveStorageTag($tag);
29+
30+
if (is_null($name)) {
31+
return new self('Storage is not registered.');
32+
}
33+
34+
return new self("Storage {$name} is not registered.");
35+
}
36+
37+
private static function resolveStorageTag(null|string|UnitEnum $name): ?string
38+
{
39+
if (is_null($name)) {
40+
return null;
41+
}
42+
43+
return is_string($name)
44+
? $name
45+
: $name->name;
1346
}
1447
}

packages/mailer/src/Exceptions/SendingMailWasForbidden.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

packages/mailer/src/GenericEmail.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Tempest\Mail;
44

55
/**
6-
* Represents a generic, basic email.
6+
* Represents a generic email.
77
*/
88
final class GenericEmail implements Email
99
{

packages/mailer/src/MailerConfig.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
namespace Tempest\Mail;
44

55
use Symfony\Component\Mailer\Transport\TransportInterface;
6-
use Tempest\Container\HasTag;
76

8-
interface MailerConfig extends HasTag
7+
interface MailerConfig
98
{
109
/**
1110
* The transport class.

packages/mailer/src/MailerInitializer.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,20 @@
44

55
use Tempest\Container\Container;
66
use Tempest\Container\DynamicInitializer;
7+
use Tempest\Container\Initializer;
78
use Tempest\Container\Singleton;
89
use Tempest\Mail\MailerConfig;
910
use Tempest\Reflection\ClassReflector;
1011
use Tempest\View\ViewRenderer;
1112
use UnitEnum;
1213

13-
final class MailerInitializer implements DynamicInitializer
14+
final class MailerInitializer implements Initializer
1415
{
15-
public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool
16-
{
17-
return $class->getType()->matches(Mailer::class);
18-
}
19-
2016
#[Singleton]
21-
public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): Mailer
17+
public function initialize(Container $container): Mailer
2218
{
2319
return new GenericMailer(
24-
mailerConfig: $container->get(MailerConfig::class, $tag),
20+
mailerConfig: $container->get(MailerConfig::class),
2521
viewRenderer: $container->get(ViewRenderer::class),
2622
);
2723
}

packages/mailer/src/Testing/MailerTester.php

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,108 @@
22

33
namespace Tempest\Mail\Testing;
44

5+
use Closure;
6+
use InvalidArgumentException;
7+
use PHPUnit\Framework\Assert;
8+
use PHPUnit\Framework\ExpectationFailedException;
59
use Tempest\Container\Container;
6-
use Tempest\Container\GenericContainer;
10+
use Tempest\Mail\Email;
711
use Tempest\Mail\Mailer;
812
use Tempest\Mail\MailerConfig;
9-
use Tempest\Mail\MailerInitializer;
10-
use Tempest\Mail\Transports\NullMailerConfig;
11-
use Tempest\Mail\Transports\Smtp\SmtpMailerConfig;
12-
use Tempest\Support\Str;
13+
use Tempest\Support\Arr;
1314
use Tempest\View\ViewRenderer;
14-
use UnitEnum;
1515

1616
final class MailerTester
1717
{
18+
private(set) ?TestingMailer $mailer = null;
19+
1820
public function __construct(
1921
private Container $container,
2022
) {}
2123

2224
/**
23-
* Forces the usage of a testing mailer.
25+
* Prevents emails from being actually sent. This is the default behavior during tests.
2426
*/
25-
public function fake(null|string|UnitEnum $tag = null): TestingMailer
27+
public function preventSendingEmails(): void
2628
{
27-
if (! $this->container->has(MailerConfig::class, $tag)) {
28-
$this->container->config(new NullMailerConfig($tag));
29-
}
30-
31-
$mailer = new TestingMailer(
32-
tag: match (true) {
33-
is_string($tag) => Str\to_kebab_case($tag),
34-
$tag instanceof UnitEnum => Str\to_kebab_case($tag->name),
35-
default => 'default',
36-
},
37-
mailerConfig: $this->container->get(MailerConfig::class, $tag),
29+
$this->mailer ??= new TestingMailer(
30+
mailerConfig: $this->container->get(MailerConfig::class),
3831
viewRenderer: $this->container->get(ViewRenderer::class),
3932
);
4033

41-
$this->container->singleton(Mailer::class, $mailer, $tag);
34+
$this->container->singleton(Mailer::class, $this->mailer);
35+
}
4236

43-
return $mailer;
37+
/**
38+
* Disables the testing mailer, so emails can actually be sent. This is usually not recommended.
39+
*/
40+
public function allowSendingEmails(): void
41+
{
42+
$this->container->unregister(Mailer::class);
4443
}
4544

4645
/**
47-
* Prevents mailers from being actually used.
46+
* Asserts that the given email class was sent.
47+
*
48+
* @param class-string<Email> $email
4849
*/
49-
public function preventRealUsage(): void
50+
public function assertSent(string $email, ?\Closure $callback = null): self
5051
{
51-
if (! ($this->container instanceof GenericContainer)) {
52-
throw new \RuntimeException('Container is not a GenericContainer, unable to prevent usage without fake.');
52+
$this->ensureTestingSetUp();
53+
$this->assertClassStringIsEmail($email);
54+
55+
$sentEmail = Arr\first($this->mailer->sent, filter: fn (Email $sent) => $sent instanceof $email);
56+
57+
Assert::assertTrue(
58+
condition: (bool) $sentEmail,
59+
message: sprintf('Email `%s` was not sent.', $email),
60+
);
61+
62+
if ($callback) {
63+
try {
64+
if ($callback($sentEmail) === false) {
65+
throw new ExpectationFailedException('The assertion callback returned `false`.');
66+
}
67+
} catch (ExpectationFailedException $previous) {
68+
throw new ExpectationFailedException(
69+
message: sprintf('Email `%s` was sent but failed the assertion.', $email),
70+
previous: $previous,
71+
);
72+
}
5373
}
5474

55-
$this->container->unregister(Mailer::class, tagged: true);
56-
$this->container->removeInitializer(MailerInitializer::class);
57-
$this->container->addInitializer(RestrictedMailerInitializer::class);
75+
return $this;
76+
}
77+
78+
/**
79+
* Asserts that the given email class was not sent.
80+
*
81+
* @param class-string<Email> $email
82+
*/
83+
public function assertNotSent(string $email): self
84+
{
85+
$this->ensureTestingSetUp();
86+
$this->assertClassStringIsEmail($email);
87+
88+
Assert::assertFalse(
89+
condition: (bool) Arr\first($this->mailer->sent, filter: fn (Email $sent) => $sent instanceof $email),
90+
message: sprintf('Email `%s` was unexpectedly sent.', $email),
91+
);
92+
93+
return $this;
94+
}
95+
96+
private function ensureTestingSetUp(): void
97+
{
98+
if (is_null($this->mailer)) {
99+
throw new ExpectationFailedException('Mail testing is not set up. Please call `$this->mailer->preventSendingEmails()` before running assertions.');
100+
}
101+
}
102+
103+
private function assertClassStringIsEmail(string $email): void
104+
{
105+
if (! is_a($email, Email::class, allow_string: true)) {
106+
throw new InvalidArgumentException(sprintf('The given email class must implement `%s`.', Email::class));
107+
}
58108
}
59109
}

0 commit comments

Comments
 (0)