Appearance
Mailing
Table of Contents
- Introduction
- Configuration
- Creating Mailables
- Writing Mailables
- Sending Mail
- Mail Templates
- Attachments
- Available Transports
- CLI Commands
- Testing
Introduction
PhenixPHP provides a clean and simple email system based on the Mailable abstraction. The system supports multiple transports (SMTP, Amazon SES, Resend) and integrates seamlessly with the view system to create attractive email templates.
The PhenixPHP mailing module:
- Supports multiple transports: SMTP, Amazon SES, Resend, and Log for testing
- Asynchronous by default: Uses AmpPHP for non-blocking async sending
- Integrated with views: Uses the template engine to render emails
- Easy to test: Includes utilities for testing with
Mail::fake() - Flexible attachments: Supports file attachments with automatic validation
Configuration
The mail system configuration is located in config/mail.php and config/services.php.
SMTP Configuration
Configuration for standard SMTP servers:
php
// config/mail.php
return [
'default' => env('MAIL_MAILER', static fn (): string => 'smtp'),
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', static fn (): string => 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', static fn (): int => 587),
'encryption' => env('MAIL_ENCRYPTION', static fn (): string => 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
],
],
'from' => [
'address' => env('MAIL_FROM_ADDRESS', static fn (): string => 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', static fn (): string => 'Example'),
],
];Environment Variables:
text
# .env
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=your_username
MAIL_PASSWORD=your_password
MAIL_FROM_ADDRESS=noreply@yourapp.com
MAIL_FROM_NAME="Your Application"Encryption Options:
tls- Transport Layer Security (port 587)ssl- Secure Sockets Layer (port 465)null- No encryption (not recommended)
Amazon SES Configuration
Configuration for Amazon Simple Email Service:
php
// config/mail.php
'mailers' => [
'ses' => [
'transport' => 'ses',
],
],php
// config/services.php
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],Environment Variables:
text
# .env
MAIL_MAILER=ses
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_DEFAULT_REGION=us-east-1Resend Configuration
Configuration for the Resend service:
php
// config/mail.php
'mailers' => [
'resend' => [
'transport' => 'resend',
],
],php
// config/services.php
'resend' => [
'key' => env('RESEND_KEY'),
],Environment Variables:
text
# .env
MAIL_MAILER=resend
RESEND_KEY=re_your_api_keyLog Configuration (Testing)
For local development and testing, the log transport records emails to logs instead of sending them:
php
// config/mail.php
'mailers' => [
'log' => [
'transport' => 'log',
],
],Environment Variables:
text
# .env
MAIL_MAILER=logNo additional configuration is required. This transport is ideal for development when you don't have an SMTP server configured.
Creating Mailables
Generating a Mailable
PhenixPHP provides the make:mail command to generate new Mailable classes:
sh
php phenix make:mail WelcomeMailThis command creates:
- The class
app/Mail/WelcomeMail.php - The view
resources/views/emails/welcome-mail.php
With nested namespaces:
sh
php phenix make:mail Auth/PasswordResetMailCreates:
app/Mail/Auth/PasswordResetMail.phpresources/views/emails/auth/password-reset-mail.php
Force overwrite:
sh
php phenix make:mail WelcomeMail --force
php phenix make:mail WelcomeMail -fMailable Structure
A generated Mailable has this structure:
php
<?php
declare(strict_types=1);
namespace App\Mail;
use Phenix\Mail\Mailable;
class WelcomeMail extends Mailable
{
public function build(): self
{
return $this->view('emails.welcome-mail')
->subject('Subject here');
}
}The build() method is where you configure the email: recipients, subject, view, attachments, etc.
Writing Mailables
Configuring the Recipient
Use the to() method to set the recipient:
php
public function build(): self
{
return $this->to('user@example.com');
}You can also pass an object that implements __toString() or has an email property:
php
public function build(): self
{
return $this->to($this->user); // $user must have 'email' property
}Configuring the Subject
Use the subject() method:
php
public function build(): self
{
return $this->to('user@example.com')
->subject('Welcome to our application!');
}Configuring the View
Use the view() method to specify the email template:
php
public function build(): self
{
return $this->to('user@example.com')
->subject('Welcome!')
->view('emails.welcome-mail');
}The view uses PhenixPHP's view system with dot notation.
View Data
To pass data to the view, use the second parameter of the view() method. This parameter accepts an associative array that will be extracted as variables in the template:
php
class WelcomeMail extends Mailable
{
public function __construct(
private readonly User $user,
private readonly string $verificationUrl,
) {}
public function build(): self
{
return $this->to($this->user->email)
->subject('Welcome to our application!')
->view('emails.welcome-mail', [
'user' => $this->user,
'verificationUrl' => $this->verificationUrl,
]);
}
}Each key in the array becomes a variable available in the view:
php
<!-- resources/views/emails/welcome-mail.php -->
<h1>Hello, {{ $user->name }}!</h1>
<p>Click the following link to verify your account:</p>
<a href="{{ $verificationUrl }}">Verify account</a>You can also pass computed values or configuration data:
php
public function build(): self
{
return $this->to($this->user->email)
->subject('Welcome!')
->view('emails.welcome-mail', [
'userName' => $this->user->name,
'appName' => config('app.name'),
'year' => date('Y'),
]);
}Note: Only the data explicitly passed in the
view()second parameter will be available as variables in the template. Properties on the Mailable class are not automatically extracted into the view scope.
Attaching Files
Use the attachment() or attachments() method:
php
public function build(): self
{
return $this->to('user@example.com')
->subject('Your invoice')
->view('emails.invoice')
->attachment('/path/to/invoice.pdf', 'Invoice-2026.pdf', 'application/pdf');
}We'll see more details about attachments later.
Sending Mail
Basic Sending
To send an email, create an instance of the Mailable and use the Mail facade:
php
use App\Mail\WelcomeMail;
use Phenix\Facades\Mail;
$user = User::find(1);
$mailable = new WelcomeMail($user, 'https://app.com/verify/123');
$future = Mail::send($mailable);
$result = $future->await(); // Wait for the result
if ($result->isSuccess()) {
// Email sent successfully
} else {
// There was an error
$error = $result->error();
}Async Sending
PhenixPHP uses AmpPHP, so email sending is asynchronous by nature. The send() method returns a Future that you can await:
php
use Amp\Future;
// Send multiple emails in parallel
$futures = [];
foreach ($users as $user) {
$mailable = new WelcomeMail($user, $verificationUrl);
$futures[] = Mail::send($mailable);
}
// Wait for all to complete
$results = Future\await($futures);
foreach ($results as $result) {
if ($result->isSuccess()) {
// Success
}
}You can also use the facade's to() method as a shortcut:
php
Mail::to('user@example.com')->send(new WelcomeMail($user, $url));Multiple Recipients
To send the same email to multiple recipients:
php
public function build(): self
{
return $this->to([
'admin@example.com',
'manager@example.com',
])
->subject('Monthly Report')
->view('emails.monthly-report');
}Or using the to() method multiple times:
php
public function build(): self
{
return $this->to('admin@example.com')
->to('manager@example.com')
->subject('Monthly Report')
->view('emails.monthly-report');
}Mail Templates
Creating a Template
Mail templates use PhenixPHP's view system. You can use all features: directives, layout inheritance, includes, etc.
php
<!-- resources/views/emails/welcome-mail.php -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
}
</style>
</head>
<body>
<h1>Hello, {{ $user->name }}!</h1>
<p>Thank you for registering with our application.</p>
@if($verificationUrl)
<p>Please verify your email address:</p>
<a href="{{ $verificationUrl }}" class="button">Verify my account</a>
@endif
<p>If you have any questions, don't hesitate to contact us.</p>
<p>Regards,<br>The {{ config('app.name') }} Team</p>
</body>
</html>Mail Layouts
Create a base layout for your emails and extend it in each template:
php
<!-- resources/views/emails/layouts/base.php -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
}
.header {
background-color: #007bff;
color: white;
padding: 20px;
text-align: center;
}
.content {
padding: 20px;
}
.footer {
background-color: #f4f4f4;
padding: 10px;
text-align: center;
font-size: 12px;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="header">
<h1>{{ config('app.name') }}</h1>
</div>
<div class="content">
@yield('content')
</div>
<div class="footer">
<p>© 2026 {{ config('app.name') }}. All rights reserved.</p>
<p>
<a href="{{ config('app.url') }}/unsubscribe">Unsubscribe</a> |
<a href="{{ config('app.url') }}/privacy">Privacy Policy</a>
</p>
</div>
</body>
</html>Now extend the layout in your emails:
php
<!-- resources/views/emails/welcome-mail.php -->
@extends('emails.layouts.base')
@section('content')
<h2>Welcome, {{ $user->name }}!</h2>
<p>We're delighted to have you with us.</p>
<p>
<a href="{{ $verificationUrl }}" class="button">Verify my account</a>
</p>
<p>If you have any questions, we're here to help.</p>
@endsectionAttachments
Attaching a File
Use the attachment() method to attach a file:
php
class InvoiceMail extends Mailable
{
public function __construct(
public readonly Invoice $invoice,
public readonly string $pdfPath,
) {}
public function build(): self
{
return $this->to($this->invoice->customer->email)
->subject("Invoice #{$this->invoice->number}")
->view('emails.invoice')
->attachment(
$this->pdfPath,
"invoice-{$this->invoice->number}.pdf",
'application/pdf'
);
}
}Parameters of the attachment() method:
path- Full path to the filename(optional) - File name for the recipientmimeType(optional) - MIME type of the file
Multiple Attachments
Use the attachments() method to attach multiple files:
php
public function build(): self
{
return $this->to('user@example.com')
->subject('Important documents')
->view('emails.documents')
->attachments([
'/path/to/document1.pdf',
'/path/to/document2.pdf',
[
'path' => '/path/to/document3.pdf',
'name' => 'Final-Contract.pdf',
'mime' => 'application/pdf',
],
]);
}Supported formats for attachments():
php
// Simple array of paths
->attachments(['/path/file1.pdf', '/path/file2.pdf'])
// Full associative array
->attachments([
[
'path' => '/path/to/file.pdf',
'name' => 'custom-name.pdf',
'mime' => 'application/pdf',
],
])
// Mixed formats
->attachments([
'/path/simple.pdf',
[
'path' => '/path/custom.pdf',
'name' => 'renamed.pdf',
],
])Automatic Validation:
The system automatically validates that attachment files exist. If a file doesn't exist, an exception will be thrown before attempting to send the email.
Available Transports
PhenixPHP supports the following transports:
SMTP Transport
Standard transport compatible with any SMTP server:
Features:
- Support for TLS/SSL
- Authentication with username and password
- Compatible with Gmail, Mailgun, SendGrid, etc.
- Configurable timeout
Use Cases:
- Own SMTP servers
- Third-party services with SMTP support
- Local development with MailHog or Mailpit
Amazon SES Transport
Amazon Web Services email service:
Features:
- High availability and scalability
- Integrated with AWS ecosystem
- CloudWatch metrics and monitoring
- Competitive pricing for high volume
Use Cases:
- Applications hosted on AWS
- High volume transactional emails
- Need for detailed metrics
Resend Transport
Modern email API service:
Features:
- Simple and modern API
- Excellent developer experience
- Intuitive dashboard
- Detailed delivery logs
Use Cases:
- Startups and modern projects
- Fast development with good DX
- Need for detailed tracking
Log Transport
Transport for development and testing:
Features:
- Does not send real emails
- Logs emails to logs
- Useful for local development
- No external configuration required
Use Cases:
- Local development without SMTP server
- Manual testing
- Debugging email content
CLI Commands
make:mail
Creates a new Mailable class along with its view:
sh
# Create a simple Mailable
php phenix make:mail WelcomeMail
# Create with nested namespace
php phenix make:mail Auth/PasswordResetMail
# Force overwrite of existing files
php phenix make:mail WelcomeMail --force
php phenix make:mail WelcomeMail -fWhat it creates:
- Mailable class in
app/Mail/ - Template view in
resources/views/emails/
Complete Example
Here is a complete example showing the typical email sending flow:
1. Create the Mailable:
sh
php phenix make:mail Orders/OrderConfirmation2. Configure the Mailable:
php
<?php
// app/Mail/Orders/OrderConfirmation.php
declare(strict_types=1);
namespace App\Mail\Orders;
use App\Models\Order;
use Phenix\Mail\Mailable;
class OrderConfirmation extends Mailable
{
public function __construct(
public readonly Order $order,
) {}
public function build(): self
{
return $this->to($this->order->customer->email)
->subject("Order confirmation #{$this->order->number}")
->view('emails.orders.order-confirmation')
->attachment(
$this->order->invoicePdfPath(),
"invoice-{$this->order->number}.pdf",
'application/pdf'
);
}
}3. Create the view:
php
<!-- resources/views/emails/orders/order-confirmation.php -->
@extends('emails.layouts.base')
@section('content')
<h2>Thank you for your order, {{ $order->customer->name }}!</h2>
<p>Your order <strong>#{{ $order->number }}</strong> has been confirmed and is being processed.</p>
<h3>Order Summary:</h3>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background-color: #f4f4f4;">
<th style="padding: 10px; text-align: left;">Product</th>
<th style="padding: 10px; text-align: center;">Quantity</th>
<th style="padding: 10px; text-align: right;">Price</th>
</tr>
</thead>
<tbody>
@foreach($order->items as $item)
<tr>
<td style="padding: 10px;">{{ $item->product->name }}</td>
<td style="padding: 10px; text-align: center;">{{ $item->quantity }}</td>
<td style="padding: 10px; text-align: right;">${{ number_format($item->price, 2) }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr style="font-weight: bold; background-color: #f4f4f4;">
<td colspan="2" style="padding: 10px; text-align: right;">Total:</td>
<td style="padding: 10px; text-align: right;">${{ number_format($order->total, 2) }}</td>
</tr>
</tfoot>
</table>
<p style="margin-top: 20px;">
You will receive a notification when your order is shipped.
</p>
<p>
<a href="{{ config('app.url') }}/orders/{{ $order->id }}" class="button">
View order details
</a>
</p>
<p>We have attached your invoice in PDF format.</p>
@endsection4. Send the email:
php
// In your controller or service
use App\Mail\Orders\OrderConfirmation;
use Phenix\Facades\Mail;
class OrderController
{
public function store(Request $request): Response
{
// Create the order
$order = Order::create($validatedData);
// Send confirmation email
$future = Mail::send(new OrderConfirmation($order));
$result = $future->await();
if ($result->isSuccess()) {
return response()->json([
'message' => 'Order created and email sent',
'order' => $order,
]);
}
// Order was created but email failed
return response()->json([
'message' => 'Order created but there was an error sending the email',
'order' => $order,
], 207); // Multi-Status
}
}5. Test:
php
use PHPUnit\Framework\TestCase;
use Phenix\Facades\Mail;
use App\Mail\Orders\OrderConfirmation;
class OrderConfirmationTest extends TestCase
{
public function test_order_confirmation_email_is_sent(): void
{
Mail::fake();
$order = Order::factory()->create();
Mail::send(new OrderConfirmation($order));
Mail::assertSent(OrderConfirmation::class, function ($mail) use ($order) {
return $mail->hasTo($order->customer->email) &&
$mail->order->id === $order->id;
});
}
}Testing
PhenixPHP provides utilities for testing email sending without actually sending them.
Mail Fake
Use Mail::fake() to intercept email sending:
php
use Phenix\Facades\Mail;
use App\Mail\WelcomeMail;
// In your test
Mail::fake();
// Code that sends emails
$user = User::factory()->create();
Mail::send(new WelcomeMail($user, 'https://verify.url'));
// Verifications (see next section)Assertions
After using Mail::fake(), you can make assertions about sent emails:
php
use Phenix\Facades\Mail;
use App\Mail\WelcomeMail;
Mail::fake();
// Your code that sends emails
$service->sendWelcomeEmail($user);
// Verify that a specific email was sent
Mail::assertSent(WelcomeMail::class);
// Verify it was sent to a specific recipient
Mail::assertSent(WelcomeMail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
// Verify an email was NOT sent
Mail::assertNotSent(PasswordResetMail::class);
// Verify the number of emails sent
Mail::assertSentCount(WelcomeMail::class, 1);Verifying Sends
You can get the emails sent during the test:
php
Mail::fake();
// Send emails
Mail::send(new WelcomeMail($user1, 'url1'));
Mail::send(new WelcomeMail($user2, 'url2'));
// Get all sends
$sent = Mail::getSendingLog();
// Verify manually
$this->assertCount(2, $sent);
$this->assertEquals($user1->email, $sent[0]['to']);Complete Test Example:
php
use PHPUnit\Framework\TestCase;
use Phenix\Facades\Mail;
use App\Mail\WelcomeMail;
use App\Services\UserService;
class UserServiceTest extends TestCase
{
public function test_welcome_email_is_sent_on_registration(): void
{
Mail::fake();
$service = new UserService();
$user = $service->register([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'secret123',
]);
// Verify that the welcome email was sent
Mail::assertSent(WelcomeMail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email) &&
$mail->user->id === $user->id;
});
}
public function test_welcome_email_contains_verification_url(): void
{
Mail::fake();
$user = User::factory()->create();
$verificationUrl = 'https://app.com/verify/abc123';
Mail::send(new WelcomeMail($user, $verificationUrl));
$sent = Mail::getSendingLog();
$this->assertCount(1, $sent);
$this->assertStringContainsString('verify/abc123', $sent[0]['body']);
}
}