I want to send a mail via laravel. For some reason, I only want to set the cc before calling the send method:
Mail::cc($cc_mail)->send(new MyMailAlert());
Then I define the recipient (to) directly in the build method of my Mailable class:
$this->subject($subject)->to($to_email)->view('my-mail');
But it fails:
Symfony\Component\Debug\Exception\FatalThrowableError: Call to undefined method Illuminate\Mail\Mailer::cc()
How can I send a mail without knowing the recipient before sending it in the build method? In other word I want to set the recipient (to) directly in the build method and I don't know how to do this.
cc is documented in Laravel Docs, but I can't find the method or property in the Illuminate\Mail\Mailer source code, neither in the Laravel API Documentation. So you can't use it this way.
But Illuminate\Mail\Mailable has the cc property. So, if you want to add the cc before sending and add the to on the build method, you need something like this:
MyMailAlert.php
class MyMailAlert extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->subject($this->subject)->to($this->to)->view('my-mail');
}
}
In your controller:
$myMailAlert = new MyMailAlert();
$myMailAlert->cc = $cc_mail;
// At this point you have cc already setted.
Mail::send($myMailAlert); // Here you sends the mail
Note that the build method uses subject and to properties of the mailable instance, so you have to set it before sending.
I'm not sure from where are you retrieving your $subject and $to_email in your build method example, but for my example you have to give these values to $myMailAlert->subject and $myMailAlert->to. You can use your custom variables in the build method, but given that the class already has these properties, custom variables aren't needed.
Here is a hack to deal with this problem:
Mail::to([])->cc($cc_mail)->send(new MyMailAlert());
So just add a to() method with an empty array and it works. It's still a hack, I'm not really sure it will work in the future.
Related
I have two different types of theme/css for my emails that are sent from my app. A default one for system emails (reset password etc) and one for consumer emails (emails as a result of actions form users within the app).
In my consumer emails, in the toMail() method of the mailable/notification I execute the mailable like so:
public function toMail($notifiable)
{
return (new MailMessage)
->theme($this->theme)
->from($this->booking->school->school_email, $this->booking->school->name)
->subject($this->getSubject())
->attachData($this->booking->payments()->first()->toPdf()->output(), 'Invoice.pdf')
->markdown('mail.notifications.bookings.paid_booking', $this->toArray());
}
Notice that I call ->theme(...) on the mailable. This works perfectly fine and the correct theme is set in the template that is received in the mailbox.
When I try to use Laravel's Mailable Preview within a route:
Route::get('/mail/resetpass', function () {
return (new App\Notifications\ResetPasswordNotification('token'))->toMail(\App\User::find(2));
});
Route::get('/mail/reserved', function () {
$booking = \App\Domains\Customers\Models\Booking::find(1);
return (new \App\Domains\Customers\Notifications\ReservedBookingConfirmation($booking))->toMail($booking->customer);
});
The "default" theme, as defined in my config files is the one that is used, and my call to ->theme(...) is ignored.
Is there a solution for this? Changing the config value, isn't a feasible option as I actually want to use this functionality to allow my users to view their emails in the browser. I'm unsure what else to try.
The issue lies in the render() method of the Illuminate\Notifications\Messages\MailMessage class.
This is fixed in Laravel 8 and so this solution only applies if your project is using laravel 7 or below
it does not call the theme. I extended the class and have overridden the render method. Adding the call to ->theme(...) like so.
<?php
namespace App\Mail;
use Illuminate\Container\Container;
use Illuminate\Mail\Markdown;
use Illuminate\Notifications\Messages\MailMessage as Original;
class MailMessage extends Original
{
/**
* Render the mail notification message into an HTML string.
*
* #return string
*/
public function render()
{
if (isset($this->view)) {
return Container::getInstance()->make('mailer')->render(
$this->view, $this->data()
);
}
return Container::getInstance()
->make(Markdown::class)
->theme($this->theme ?? config('mail.markdown.theme')) //add this line here
->render($this->markdown, $this->data());
}
}
In my Laravel 8 project I try to send HTML e-mail, but get Undefined variable error.
I have this code in my controller:
// here the $client has a model value
Mail::to($client)->send(new ClientCreated($client));
In my app/Mail/ClientCreated.php file:
<?php
namespace App\Mail;
use App\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ClientCreated extends Mailable
{
use Queueable, SerializesModels;
private $client;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
// here the $this->client has a model value
return $this->view('emails.client.created');
}
}
Finally in my resources/views/emails/client/created.blade.php I have this code:
<p>Dear{{ $client->name }}!</p>
And I got this error message:
Undefined variable: client (View: /home/vagrant/Code/myproject/laravel/resources/views/emails/client/created.blade.php)
I read the docs and search on the Stackoverflow, but not found any help.
Any idea what I made wrong?
If you passed the variable to the view properly, but it still does not work, try to restart the queue in the console:
php artisan queue:restart
You should make $client public not private:
public $client;
"There are two ways you may make data available to your view. First, any public property defined on your mailable class will automatically be made available to the view"
Laravel 8.x Docs - Mail - Writing Mailables - View Data - Via Public Properties
The other method would be calling with:
$this->view(...)->with('client', $this->client);
"If you would like to customize the format of your email's data before it is sent to the template, you may manually pass your data to the view via the with method."
Laravel 8.x Docs - Mail - Writing Mailables - View Data - Via the with Method
If you do not want to change $client to public then use the second method.
You should make $client public
public $client;
Once the data has been set to a public property, it will automatically be available in your view, so you may access it like you would access any other data in your Blade templates.
https://laravel.com/docs/8.x/mail#view-data
In a test I would like to make some assertions on a Mailable using Mail::assertSent(), like this:
Mail::assertSent(MyMailable::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
So far, I have found the hasTo(), hasFrom(), hasBcc() and all the other has*() methods work just great. However, when asserting a particular attribute on the Mailable exists, for example subject the attribute shows up as null and the assertion fails:
Mail::assertSent(MyMailable::class, function ($mail) {
return $mail->subject === 'My Subject';
});
I believe this is because I have configured all of the Mailable attributes within the build() method which at the stage of the assertion probably hasn't been invoked, so the attributes are not set on the object yet.
I thought using the build() method was the correct approach to take based on the docs:
All of a mailable class' configuration is done in the build method. Within this method, you may call various methods such as from, subject, view, and attach to configure the email's presentation and delivery.
https://laravel.com/docs/5.5/mail#writing-mailables
I have found that I can get assertions on the Mailable's attributes working when I instead set the attributes on the constructor:
class MyMail extends Mailable
{
public function __construct()
{
$this->subject = 'My Subject';
}
public function build() {
return $this->subject('My Subject')->view('emails/my-email')
}
}
However, I feel this approach is wrong because I feel like I am changing my code to suit my tests.
So, I would like to know if there is a better approach to making assertions against attributes on a Mailable? Any help would be most appreciated, thank you!
EDIT 1
Test class (irrelevant code stripped out)
/** #test */
function a_notification_is_sent_when_an_application_is_updated()
{
Mail::fake([RequiresVerification::class]);
// some set up and factory methods called here...
// the listener for this event sends mail
ApplicationUpdated::dispatch($application);
// this assertion passes
Mail::assertSent(RequiresVerification::class);
// this assertion does not pass when subject is set on the build()
// method but passes when subject is set on the constructor
Mail::assertSent(RequiresVerification::class, function ($mail) use ($user) {
return $mail->subject === 'hello';
});
}
EDIT 2
I am currently looking at the hasRecipient() method, which all has* methods use, to see how it handles making assertions against what I assume are Mailable attributes (to, from, bcc, cc, etc). Perhaps the Mailable object can be extended to add new attribute assertions using a similar approach?
https://github.com/laravel/framework/blob/5.5/src/Illuminate/Mail/Mailable.php#L540
You can make assertions against attributes configured in the build() method by calling the build() method within the assertSent closure before making the assertions:
Mail::assertSent(MyMailable::class, function ($mail) {
$mail->build();
return $mail->subject === 'My Subject';
});
Thanks to #ohffs on laracasts for helping with this: https://laracasts.com/discuss/channels/testing/how-to-make-assertions-on-a-laravel-mailable
I am attempting to setup automated email notification in my Laravel 5.5 app using Mailgun. I have the Mailgun SDK installed along with the recommended Laravel library - Bogardo. The reason I am using the Bogardo library instead of just using the Mailgun SDK or built in Laravel email functionality is neither of these allow for click tracking, bounces and other analytic functionality (that I know of). I am able to send emails just fine using Tinker. However, I am not 100% sure how to properly call my new mailable to send an email that way. Here is my mailable class:
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class BaseEmail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
$data = ['This is a message from Mailgun!'];
return Mailgun::raw($data, function($message) {
$message
->to('email#domain.com', 'Name Name')
->subject('Yoohoo!')
->from('otheremail#domain.com', 'Name')
->tag(['tag','tag2']);
});
}
}
When I call:
$mail = new App\Mail\BaseEmail();
$mail->send();
I get the following error
TypeError: Too few arguments to function Illuminate\Mail\Mailable::send(), 0 passed in /web/vendor/psy/psysh/src/Psy/ExecutionLoop/Loop.php(90) : eval()'d code on line 1 and exactly 1 expected
and
$mail->send('this');
I get
TypeError: Argument 1 passed to Illuminate\Mail\Mailable::send() must be an instance of Illuminate\Contracts\Mail\Mailer, string given on line 1
Sorry if this is trivial, but I have been following their docs and have Googled everything that I can think of with no luck.
Any direction would be fantastic!
Thanks!
It appears that the Bogardo package does not support Laravel Mailables...
https://github.com/Bogardo/Mailgun/issues/72
You don't need to use the send() method when you're using raw().This code will send an email:
Mailgun::raw($data, function($message) {
$message->to('email#domain.com', 'Name Name')
->subject('Yoohoo!')
->from('otheremail#domain.com', 'Name')
->tag(['tag','tag2']);
});
You also don't need to use Mailable when you're using the package.
I am realizing that perhaps the way I want to make use of the Validator Component from Symfony is not possible. Here is the idea.
I have a class called Package which for now has only one property named namespace. Usually I would include the ClassMetadata and any constraint object I would like to validate against within my Package class. However, my idea is that instead of doing that I would rather keep my subject clean and only responsible for the things it must be responsible for.
Below is a class I wrote and call it PackageValidater:
<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validation;
class PackageValidator
{
protected $subject;
public function PackageValidator($subject){
$this->subject = $subject;
}
public static function loadMetadata(){
$metadata->addPropertyConstraint('namespace', new new Assert\Type(['type' => 'string']));
}
public function getViolations(){
$validator = Validation::createValidatorBuilder()
->addMethodMapping('loadMetadata')
->getValidator();
$violations = $validator->validate($this->subject);
return !empty($violations) ? $violations : [];
}
}
Despite of the fact that I am not sure about the usage of my constraint since most reference uses annotations and I do not we can ignore that part. I also am aware of the fact that my test fails due to this fact. However, my issue is with my design because I have not added the static function that the Validation object uses to build the validation. Instead of my method mapping where constraints reside being in the actual object it resides on a separate class.
The idea is to enforce separation of concerns and single responsibility on my objects. Below is a diagram that depicts exactly what I am trying to achieve:
I have written my test as shown below:
$packageValidator = new PackageValidator(new Package([0 => 'test']));
$this->assertTrue(true, empty($packageValidator->getViolations()));
Above I have passed in an array instead of a string which would make my test fail because there can never be a single namespace that is in a form of array - at least not in what I am trying to achieve.
The issue is with my getViolations method inside the PackageValidator object because I am not passing my subject outside the context of my validation process that is define the subject metadata inside the subject itself then when getting the validator object with the refence to the subject's metadata get the validation errors.
All in all Package does not have loadMetadata method but PackageValidator. How can I make this possible without polluting every object I want to validate with the metadata functionality?
Below is what I get from PHPUnit:
SimplexTest\Validate\Package\PackageValidatorTest::testIfValidatorInterfaceWorks
Symfony\Component\Validator\Exception\RuntimeException: Cannot
validate values of type "NULL" automatically. Please provide a
constraint.
You can use yml or xml configuration to add constraints to your object.
http://symfony.com/doc/current/book/validation.html#the-basics-of-validation
You do this by creating a file called validation.yml in your Bundle configuration directory. Add the following content to validate your object:
Some\Name\Space\Package:
properties:
name:
- NotBlank: ~
That's one way to keep things you don't consider a responsibility for your object out of said object. It also removes the need for a custom validator class for every object you create. You can simply make use of the validator service already provided by the framework.
Edit
Alright I think I figured something out you might be looking for: you can create a MetadataFactory to load Metadata the way you want. There are a couple of examples here: https://github.com/symfony/validator/tree/master/Mapping/Factory
It basically boils down to a Factory class that returns an instance of MetadataInterface where you attach your constraints. This means that you can have the Factory read metadata from anything. You could for example do something like this:
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
use Your\Package;
class PackageMetadataFactory implements MetadataFactoryInterface
{
/**
* Create a ClassMetaData object for your Package object
*
* #param object $value The object that will be validated
*/
public function getMetadataFor($value)
{
// Create a class meta data object for your entity
$metadata = new ClassMetadata(Package::class);
// Add constraints to your metadata
$metadata->addPropertyConstraint(
'namespace', new Assert\Type(['type' => 'string']));
// Return the class metadata object
return $metadata;
}
/**
* Test if the value provided is actually of type Package
*
* #param object $value The object that will be validated
*/
public function hasMetadataForValue($value)
{
return $value instanceof Package::class;
}
}
Then in your PackageValidator all you have to do is:
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validation;
use Your\PackageMetadataFactory;
class PackageValidator
{
protected $subject;
public function PackageValidator($subject) {
$this->subject = $subject;
}
public function getViolations() {
$validator = Validation::createValidatorBuilder()
->setMetadataFactory(new PackageMetadataFactory())
->getValidator();
$violations = $validator->validate($this->subject);
return !empty($violations) ? $violations : [];
}
}
Hopefully this is more in line of what you're looking for.
I have followed your suggestion above as you have put it. The only thing I had to change was the hasMetadaFor method implementation inside the PackageMetadataFactory. Below is how I rather check for property existence.
public function hasMetadataFor( $value ){
return property_exists(Package::class, $value);
}
Everything else as you suggested works perfectly. Below is my test function.
$validator = new PackageValidator(new OrderPackage(125787618));
$this->assertSame(true, $validator->validates());
The test fails because the namespace cannot be numbers. Passing the fully qualified class name of the OrderPackage by doing OrderPackage ::class validates the object.
Thank you very much for your advice.