Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Alternative Mailable Syntax #44462

Merged
merged 23 commits into from
Oct 11, 2022
Merged

[9.x] Alternative Mailable Syntax #44462

merged 23 commits into from
Oct 11, 2022

Conversation

taylorotwell
Copy link
Member

@taylorotwell taylorotwell commented Oct 4, 2022

Currently, mailables are "configured" via a build method. Within this method, various methods on the parent mailable class are invoked to specify the structure of the mailable:

public function build()
{
    return $this->from('[email protected]', 'Example')
                ->subject('Order Shipped')
                ->view('emails.orders.shipped');
}

While this works fine, over the last couple of years it has always rubbed me the wrong way. It feels a lot like an initialization method or a two-step constructor. The mailable object can't be inspected in any useful way until that method is invoked. There aren't really any other types of objects in Laravel user-land that work this way or follow this pattern.

This PR offers an alternative syntax for defining mailables in which several discrete methods may be defined that each return configuration style objects that describe the structure of the mailable:

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class InvoicePaid extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the message envelope.
     *
     * @return \Illuminate\Mail\Mailables\Envelope
     */
    public function envelope()
    {
        return new Envelope(
            subject: 'Invoice Paid',
            cc: [new Address('[email protected]', 'Example Name')],
            tags: [],
            metadata: [],
        );
    }

    /**
     * Get the message content definition.
     *
     * @return \Illuminate\Mail\Mailables\Content
     */
    public function content()
    {
        return new Content(
            view: 'html-view-name',
            text: 'text-view-name',
        );
    }

    /**
     * Get the attachments for the message.
     *
     * @return \Illuminate\Mail\Mailables\Attachment[]
     */
    public function attachments()
    {
        return [
            Attachment::fromPath('/path/to/file'),
        ];
    }
}

Note: Only the content method is really required when defining mailables in this fashion.

A more minimal example would look like the following:

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Queue\SerializesModels;

class InvoicePaid extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Get the message content definition.
     *
     * @return \Illuminate\Mail\Mailables\Content
     */
    public function content()
    {
        return new Content(
            markdown: 'invoice-paid',
        );
    }
}

The DX of this feature relies heavily on PHP 8 named arguments to provide a nice, clean experience when defining the configuration of the mailable object.

While the resulting mailable does end up having more lines of code overall, it feels a bit more like a real object and not a weird, two-step constructed thing. The envelope, content, and attachments methods may all be invoked as many times as needed and their results inspected like normal objects.

A few new methods have also been added to inspect mailable instances:

$mailable->hasSubject('Foo');
$mailable->hasTag('foo');
$mailable->hasMetadata('key', 'value');

In addition, an optional headers method may be defined to add additional headers, including arbitrary text headers to the message:

use Illuminate\Mail\Mailables\Headers;

public function headers()
{
    return new Headers(
        messageId: '[email protected]',
        references: ['[email protected]', '[email protected]'],
        text: [
            'X-Some-Header' => 'Some-Value',
        ],
    );
}

The traditional way of defining mailables using the build method will not be removed.

@Sammyjo20
Copy link
Contributor

Sammyjo20 commented Oct 6, 2022

I really like the syntax, but I have a suggestion to ?maybe? improve readability, of course you could write it with the constructor arguments, but what about methods too?

public function envelope()
{
    $envelope = new Envelope('Invoice Paid');
      
    // addRecipient ($address, $type = 'to')
    $envelope->addRecipient(new Address('[email protected]', 'Example Name'), type: 'cc');
    $envelope->addRecipient(new Address('[email protected]', 'Example Name'), type: 'bcc');
   
    $envelope->addTags([]);

    $envelope->addMetadata([]);

   return $envelope;
}

@Sammyjo20
Copy link
Contributor

In fact methods will be really helpful to conditionally apply logic like, if in production always CC the sales email address.

@adityakdevin
Copy link

Syntax looks great

@hailwood
Copy link
Contributor

hailwood commented Oct 6, 2022

In fact methods will be really helpful to conditionally apply logic like, if in production always CC the sales email address.

So perhaps as well as the methods we could add the Conditionable trait as well so we can use when/unless chaining?

@nunomaduro nunomaduro changed the title Alternative Mailable Syntax [9.x] Alternative Mailable Syntax Oct 6, 2022
@taylorotwell
Copy link
Member Author

cc: ['[email protected]'] + ($production ? ['[email protected]'] : []),

@hailwood
Copy link
Contributor

hailwood commented Oct 6, 2022

cc: ['[email protected]'] + ($production ? ['[email protected]'] : []),

Sure that's possible but it's not exactly nice to scan, especially if you're using the full address syntax?

return new Envelope(
    subject: 'Invoice Paid',
    cc: [new Address('[email protected]', 'Taylor Otwell')] + ($production ? [new Address('[email protected]', 'Second Address')] : []),
    tags: [],
    metadata: [],
);

I'd be more inclined to go

$ccRecipients = [new Address('[email protected]', 'Taylor Otwell')];

if($production) {
    $ccRecipients[] = new Address('[email protected]', 'Second Address');
}

return new Envelope(
    subject: 'Invoice Paid',
    cc: $ccRecipients,
    tags: [],
    metadata: [],
);

But isn't it easier to understand if it's just

$envelope = new Envelope(
    subject: 'Invoice Paid',
    cc: [new Address('[email protected]', 'Taylor Otwell')],
    tags: [],
    metadata: [],
));

if($production) {
    $envelope->addRecipient(new Address('[email protected]', 'Second Address'), type: 'cc');
}

return $envelope;

Or if conditionable then

return (new Envelope(
    subject: 'Invoice Paid',
    cc: [new Address('[email protected]', 'Taylor Otwell')],
    tags: [],
    metadata: [],
)))->when($production, function(Envelope $envelope) {
    $envelope->addRecipient(new Address('[email protected]', 'Second Address'), type: 'cc');
});

Copy link
Member

@timacdonald timacdonald left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggesting we explicitly mark constructors with named argument support.

This is a custom docblock, similar to what is seen in PHPUnit (however they are opting out of named argument support).

As Laravel does not support named arguments globally, this will serve as a marker for developers and maintainers that the feature is supported here.

src/Illuminate/Mail/Mailables/Headers.php Outdated Show resolved Hide resolved
src/Illuminate/Mail/Mailables/Envelope.php Outdated Show resolved Hide resolved
src/Illuminate/Mail/Mailables/Content.php Outdated Show resolved Hide resolved
Copy link
Contributor

@lioneaglesolutions lioneaglesolutions left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would indeed be nice to add the fluent methods - have suggested some changes including the tests.

See;

src/Illuminate/Mail/Mailables/Envelope.php Show resolved Hide resolved
tests/Mail/MailableAlternativeSyntaxTest.php Show resolved Hide resolved
@andrey-helldar
Copy link
Contributor

andrey-helldar commented Oct 7, 2022

For me personally, explicitly passing constructor properties is redundant and ugly.

So agree with @Sammyjo20 for adding methods.

It is also necessary to be able to use beautiful code. For example:

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class InvoicePaid extends Mailable
{
    use Queueable, SerializesModels;

    public function envelope()
    {
        return (new Envelope())
            ->setSubject('Invoice Paid')
            ->addRecipient(new Address('[email protected]', 'Example Name'), type: 'cc')
            ->addRecipient(new Address('[email protected]', 'Example Name'), type: 'bcc')
            ->addTags(['foo', 'bar'])
            ->addMetadata([]);
    }

    public function content()
    {
        return new Content(
            view: 'html-view-name',
            text: 'text-view-name',
        );
    }
}

@nathanjansen
Copy link

Just another possible syntax.

public function envelope()
{
    return new Envelope(
        subject: 'Invoice Paid',
        cc: Envelope\CC::make()
            ->add(new Address('[email protected]', 'Example Name'))
            ->when($production, new Address('[email protected]', 'Example Admin')),
        tags: [],
        metadata: [],
    );
}

@laravel laravel deleted a comment from garygreen Oct 7, 2022
@taylorotwell
Copy link
Member Author

Added methods and conditionable trait to Envelope.

@Sammyjo20
Copy link
Contributor

Methods look good!

@taylorotwell
Copy link
Member Author

Documentation: laravel/docs#8282

@ghost
Copy link

ghost commented Dec 8, 2022

How can we use this syntax into APIs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.