How to make assertions on a Laravel Mailable - php

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

Related

Laravel Mailable Preview not taking account of the ->theme(...) method call on Mailable

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());
}
}

Sending a mail without `to` method in Laravel

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.

Symfony Validator Component issue in Standalone Applicatin

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.

Call to a member function success() on a non-object

I have tried many times this error in different ways also tried one sollution from: CakePHP: Call to a member function setFlash() on a non-object url ... But this solution is also not working in my project.
Cant recognize whats the error. !!!
Here is my User.php model
class User extends AppModel {
public $helpers = array('Html','Form');
public $components = array('Email','Flash','Session');
public function send_mail($useremail){
$Email = new CakeEmail('gmail');
$Email->emailFormat('html')
->from('abc#xyz.com')
->to($useremail)
->subject('User subject');
if($Email->send("123")){
$this->Flash->success(__('Mail Sent')); // **this line cause error**
}else{
$this->Session->setFlash('Problem during sending email');
}
}
}
As the documentation suggest, FlashComponent provides two ways to set flash messages: its __call()magic method and its
set() method. To furnish your application with verbosity, FlashComponent’s __call() magic method allows you use a method name that maps to an element located under the src/Template/Element/Flash directory. By convention, camelcased methods will map to the lowercased and underscored element name:
// Uses src/Template/Element/Flash/success.ctp
$this->Flash->success('This was successful');
// Uses src/Template/Element/Flash/great_success.ctp
$this->Flash->greatSuccess('This was greatly successful');
Alternatively, to set a plain-text message without rendering an element, you can use the set() method:
$this->Flash->set('This is a message');
And hence, you need to change it by typing
$this->Flash->success('Mail sent');

Set validation message

I am trying to set a validation message for a set_rule....is this possible?
I tried this
$this->form_validation->set_rules('payment_amount', 'Payment Amount', 'regex_match[/^\d{0,4}(\.\d{1,2})?$/]');
$this->form_validation->set_message('payment_amount', 'This is a test');
and my message did not change.
Any ideas?
You can set a custom validation error message by creating a function for the rule.
This code is untested.
public function payment_amount($str)
{
if (preg_match('[/^\d{0,4}(\.\d{1,2})?$/]', $str)
{
$this->form_validation->set_message('payment_amount', 'The %s field has an error');
return FALSE;
}
else
{
return TRUE;
}
}
Us CI's callback as follows:
$this->form_validation->set_rules('payment_amount', 'Payment Amount', 'callback_payment_amount')
Create MY_Form_validation.php & put your code into.
Use the documentation (http://ellislab.com/codeigniter/user-guide/general/creating_libraries.html)
Extending Native Libraries
If all you need to do is add some functionality to an existing library - perhaps add a function or two - then it's overkill to replace the entire library with your version. In this case it's better to simply extend the class. Extending a class is nearly identical to replacing a class with a couple exceptions:
The class declaration must extend the parent class.
Your new class name and filename must be prefixed with MY_ (this item is configurable. See below.).
For example, to extend the native Email class you'll create a file named application/libraries/MY_Email.php, and declare your class with:
class MY_Email extends CI_Email {
}
Note: If you need to use a constructor in your class make sure you extend the parent constructor:
class MY_Email extends CI_Email {
public function __construct()
{
parent::__construct();
}
}

Categories