Skip to content

Commit

Permalink
Add Support for new Mailable syntax (#14)
Browse files Browse the repository at this point in the history
* Add Mailables with new syntax for tests

* Add Tests to cover new syntax

* Add new methods to get raw headers

* Refactor associateWith method in StoreMailables

* Make public methods public (doesn't really matter here, but indicates which methods should be used in user land)

* Document new methods in the README
  • Loading branch information
stefanzweifel committed Nov 5, 2022
1 parent 6d597d2 commit b9a5526
Show file tree
Hide file tree
Showing 11 changed files with 771 additions and 19 deletions.
129 changes: 121 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ Read further to learn how to store the name and how to associate models with a M
By default the Event Listener stores the mails subject and the recipient adresses. That's nice, but we can do better.
It can be beneficial for your application to know which Mailable class triggered the sent email.

To store this information, add the `StoreMailables`-trait to your Mailable classes like below.
Then call the `storeClassName()`-method inside the `build`-method.
To store this information, add the `StoreMailables`-trait to your Mailable classes like below. You now have access to a couple of helper methods.

Depending on how you write Mailables, there a different ways to use these new methods.
Call the `storeClassName`-method inside the `build`-method of your Mailable.

```php
class ProductReviewMail extends Mailable
Expand All @@ -151,6 +153,42 @@ class ProductReviewMail extends Mailable
}
```

If you use the Mailable syntax introduced in Laravel v9.35, you can either use `$this->storeClassName()` in the `headers`-method or pass `$this->getMailClassHeader()->toArray()` to the `Header` object.

```php
class ProductReviewMail extends Mailable
{
use SerializesModels;
use StoreMailables;

public function __construct(
public User $user,
public Product $product
) { }

// ...

/**
* @return \Illuminate\Mail\Mailables\Headers
*/
public function headers()
{
// Call storeClassName() and let the package take care of adding the
// header to the outgoing mail.
$this->storeClassName();

// Or – if you want more control – use the getMailClassHeader() method
// to get a Header instance and merge it with your own headers.
return new Headers(
text: [
'X-Custom-Header' => 'Custom Value',
...$this->getMailClassHeader()->toArray(),
],
);
}
}
```

The method will add a `X-Laravel-Mail-Class`-header to the outgoing email containing the fully qualified name (FQN) of the Mailable class as an encrypted string. (eg. `App\Mails\ProductReviewMail`). Update the `SENDS_HEADERS_MAIL_CLASS`-env variable to adjust the header name. (See config for details).

The package's event listener will then look for the header, decrypt the value and store it in the database.
Expand Down Expand Up @@ -180,9 +218,10 @@ class ProductReviewMail extends Mailable
use SerializesModels;
use StoreMailables;

public function __construct(private User $user, private Product $product)
{
}
public function __construct(
private User $user,
private Product $product
) { }

public function build()
{
Expand All @@ -196,7 +235,45 @@ class ProductReviewMail extends Mailable
}
```

You can now access the sent out emails from the product's `send`-relationship.
If you're using the Mailable syntax introduced in Laravel v9.35, you can call `associateWith()` or `getMailModelsHeader()` in the `headers`-method too.

```php
class ProductReviewMail extends Mailable
{
use SerializesModels;
use StoreMailables;

public function __construct(
private User $user,
private Product $product
) { }

// ...

/**
* @return \Illuminate\Mail\Mailables\Headers
*/
public function headers()
{
// Call associateWith() and the package automatically associates the public
// properties with this mailable.
$this->associateWith($this->product);
$this->associateWith([$this->product]);

// Or – if you want more control – use the getMailModelsHeader() method
// to get a Header instance and merge it with your own headers.
return new Headers(
text: [
'X-Custom-Header' => 'Custom Value',
...$this->getMailModelsHeader($this->product)->toArray(),
...$this->getMailModelsHeader([$this->product])->toArray(),
],
);
}
}
```

You can now access the sent out emails from the product's `sends`-relationship.

```php
$product->sends()->get();
Expand Down Expand Up @@ -229,6 +306,42 @@ class ProductReviewMail extends Mailable
}
```

If you're using the Mailable syntax introduced in Laravel v9.35, you can call `associateWith()` or `getMailModelsHeader()` in the `headers`-method.

```php
class ProductReviewMail extends Mailable
{
use SerializesModels;
use StoreMailables;

public function __construct(
private User $user,
public Product $product
) { }

// ...

/**
* @return \Illuminate\Mail\Mailables\Headers
*/
public function headers()
{
// Call associateWith() and the package automatically associates the public
// properties with this mailable.
$this->associateWith();

// Or – if you want more control – use the getMailModelsHeader() method
// to get a Header instance and merge it with your own headers.
return new Headers(
text: [
'X-Custom-Header' => 'Custom Value',
...$this->getMailModelsHeader()->toArray(),
],
);
}
}
```

### Attach custom Message ID to Mails

If you're sending emails through AWS SES or a similar service, you might want to identify the sent email in the future (for example when a webhook for the "Delivered"-event is sent to your application).
Expand Down Expand Up @@ -267,7 +380,7 @@ SENDS_STORE_CONTENT=true

If you need to store more attributes with your `Send`-model, you can extend the `StoreOutgoingMailListener` and override the `getSendAttributes`-method.

For example, let's say we would like to store an `audience`-value with each sent out email. We create a new Event Listtener called `CustomStoreOutgoingMailListener` and use the class a as Listener to the `MessageSent`-event.
For example, let's say we would like to store an `audience`-value with each sent out email. We create a new Event Listener called `CustomStoreOutgoingMailListener` and use the class as Listener to the `MessageSent`-event.

Our `EventServiceProvider` would look like this.

Expand All @@ -281,7 +394,7 @@ protected $listen = [
]
```

The Listener itself would look like the code below. We extend `Wnx\Sends\Listeners\StoreOutgoingMailListener` and override `getSendAttributes`. We merge the `$defaultAttributes` with our custom attributes we want to store. In our example we store an `audience` value.
The Listener itself would look like the code below. We extend `Wnx\Sends\Listeners\StoreOutgoingMailListener` and override `getSendAttributes`. We merge the `$defaultAttributes` with our custom attributes we want to store. In the example below we store an `audience` value.

```php
<?php
Expand Down
17 changes: 17 additions & 0 deletions src/Header.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Wnx\Sends;

use Symfony\Component\Mime\Header\UnstructuredHeader;

class Header extends UnstructuredHeader
{
public function toArray(): array
{
return [
$this->getName() => $this->getValue(),
];
}
}
58 changes: 47 additions & 11 deletions src/Support/StoreMailables.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,61 @@

namespace Wnx\Sends\Support;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\Mime\Email;
use Wnx\Sends\Contracts\HasSends;
use Wnx\Sends\Header;

trait StoreMailables
{
protected function storeClassName(): self
public function storeClassName(): self
{
$this->withSymfonyMessage(function (Email $message) {
$message->getHeaders()->addTextHeader(config('sends.headers.mail_class'), encrypt(self::class));
$header = $this->getMailClassHeader();

$message->getHeaders()->addTextHeader($header->getName(), $header->getValue());
});

return $this;
}

public function getMailClassHeader(): Header
{
return new Header(
name: config('sends.headers.mail_class'),
value: encrypt(self::class)
);
}

/**
* @param array<HasSends>|HasSends $models
* @return StoreMailables
* @throws \ReflectionException
*/
protected function associateWith(array|HasSends $models = []): static
public function associateWith(array|HasSends $models = []): static
{
$models = $models instanceof HasSends ? func_get_args() : $models;

$models = collect($models)
$this->withSymfonyMessage(function (Email $message) use ($models) {
$message->getHeaders()->addTextHeader(
config('sends.headers.models'),
encrypt($this->getCollectionOfAssociatedModels($models)->toJson())
);
});

return $this;
}

/**
* @param array<Model> $models
* @return Collection
* @throws \ReflectionException
*/
private function getCollectionOfAssociatedModels(array $models): Collection
{
return collect($models)
->when(count($models) === 0, function (Collection $collection) {
$publicPropertyWithHasSends = collect((new ReflectionClass($this))
->getProperties(ReflectionProperty::IS_PUBLIC))
Expand All @@ -43,13 +71,21 @@ protected function associateWith(array|HasSends $models = []): static
->map(fn (HasSends $model) => [
'model' => get_class($model),
'id' => $model->getKey(),
])
->toJson();
]);
}

$this->withSymfonyMessage(function (Email $message) use ($models) {
$message->getHeaders()->addTextHeader(config('sends.headers.models'), encrypt($models));
});
/**
* @param array<HasSends>|HasSends $models
* @return Header
* @throws \ReflectionException
*/
public function getMailModelsHeader(array|HasSends $models = []): Header
{
$models = $models instanceof HasSends ? func_get_args() : $models;

return $this;
return new Header(
name: config('sends.headers.models'),
value: encrypt($this->getCollectionOfAssociatedModels($models)->toJson()),
);
}
}
Loading

0 comments on commit b9a5526

Please sign in to comment.