I am working on a Laravel project. I am writing integration/ feature tests for my application. I am now writing a test where I need to assert the data passed to the email notification and the data passed to its view. I found this link to do it, https://medium.com/#vivekdhumal/how-to-test-mail-notifications-in-laravel-345528917494.
This is my notification class
class NotifyAdminForHelpCenterCreated extends Notification
{
use Queueable;
private $helpCenter;
public function __construct(HelpCenter $helpCenter)
{
$this->helpCenter = $helpCenter;
}
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
return (new MailMessage())
->subject("Help Center registration")
->markdown('mail.admin.helpcenter.created-admin', [
'helpCenter' => $this->helpCenter,
'user' => $notifiable
]);
}
}
As you can see in the code, I am passing data to mail.admin.helpcenter.created-admin blade view.
This is my test method.
/** #test */
public function myTest()
{
$body = $this->requestBody();
$this->actingAsSuperAdmin()
->post(route('admin.help-center.store'), $body)
->assertRedirect();
$admin = User::where('email', $body['admin_email'])->first();
$helpCenter = HelpCenter::first();
Notification::assertSentTo(
$admin,
NotifyAdminForHelpCenterCreated::class,
function ($notification, $channels) use ($admin, $helpCenter) {
$mailData = $notification->toMail($admin)->toArray();
//here I can do some assertions with the $mailData
return true;
}
);
}
As you can see my comment in the test, I can do some assertions with the $mailData variable. But that does not include the data passed to the view. How can I assert or get the data or variables passed to the blade view/ template?
As you can see here, there is a viewData property on the MailMessage class which contains all the data passed to the view, no need to turn the notification into an array.
$notification->toMail($admin)->viewData
So it would be something like this in your case:
/** #test */
public function myTest()
{
$body = $this->requestBody();
$this->actingAsSuperAdmin()
->post(route('admin.help-center.store'), $body)
->assertRedirect();
$admin = User::where('email', $body['admin_email'])->first();
$helpCenter = HelpCenter::first();
Notification::assertSentTo(
$admin,
NotifyAdminForHelpCenterCreated::class,
function ($notification, $channels) use ($admin, $helpCenter) {
$viewData = $notification->toMail($admin)->viewData;
return $admin->is($viewData['user']) && $helpCenter->is($viewData['helpCenter']);
}
);
}
Related
Laravel Version: 8.78.1
PHP Version: 8.0.10
I've created a custom command to run on a schedule and email a notification.
My Command class handle method:
public function handle()
{
$sql = "SELECT * FROM Licences WHERE (Expired = 1)";
$list = DB::select($sql);
return (new NotifyExpiredLicences($list))->toMail('me#gmail.com');
}
My notification method:
public function toMail($notifiable)
{
return (new MailMessage)
->subject('Clients with Expired Licences')
->markdown('vendor/notifications/expiredlicences',
['clients' => $this->list, 'toname' => 'Me']);
}
Whenever I test this by running it manually with php artisan email:expired-licences I get the following error Object of class Illuminate\Notifications\Messages\MailMessage could not be converted to int from my command class in the handle method.
However, the preview of my email works fine & displays as expected:
Route::get('/notification', function () {
return (new SendExpiredLicences())->handle();
});
If I remove the return statement from my handle() method, then although I get no errors, neither in my console or in storage\logs, also the preview stops working.
At this point I'm sure I've missed something important from the way this is supposed to be done, but after going through the Laravel docs and looking at online tutorials/examples, I've no idea what.
I've got everything working - though not entirely sure it's the "Laravel way".
If anyone's got suggestions for improving it - add a comment or new answer and I'll try it out.
Console\Kernel.php:
protected function schedule(Schedule $schedule)
{
$schedule->command('email:expired-licences')
->weekdays()
->at('08:30');
}
App\Console\Commands\SendExpiredLicences.php:
class SendExpiredLicences extends Command
{
protected $signature = 'email:expired-licences';
protected $description = 'Email a list of expired licences to Admin';
private $mail;
public function _construct()
{
$clients = DB::select("[Insert SQL here]");
$this->mail = (new NotifyExpiredLicences($clients))->toMail('admin#example.com');
parent::__construct();
}
public function handle()
{
Mail::to('admin#example.com')->send($this->mail);
return 0;
}
public function preview()
{
return $this->mail;
}
}
App\Notifications\NotifyExpiredLicences.php:
class NotifyExpiredLicences extends Notification
{
public function __construct(protected $clients)
{
}
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
return (new Mailable($this->clients));
}
}
App\Mail\ExpiredLicences.php:
class ExpiredLicences extends Mailable
{
public function __construct(private $clients)
{
}
public function build()
{
return $this
->subject('Clients with Expired Licences')
->markdown('emails/expiredlicences',
['clients' => $this->clients, 'toname' => 'Admin']);
}
}
resources\views\emails\expiredlicences.blade.php:
#component('mail::message')
# Hi {!! $toname !!},
#component('mail::table')
| Client | Expired |
| ------------- | --------:|
#foreach ($clients as $client)
|{!! $client->CompanyName !!} | {!! $client->Expired !!}|
#endforeach
#endcomponent
<hr />
Thanks, {!! config('app.name') !!}
#endcomponent
For previewing with the browser routes\web.php:
Route::get('/notification', function () {
return (new SendExpiredLicences())->preview();
});
Ok just to save more commenting, here's what I'd recommend doing. This is all based on the Laravel docs, but there are multiple ways of doing it, including what you've used above. I don't really think of them as "right and wrong," more "common and uncommon."
Console\Kernel.php: I'd keep this mostly as-is, but pass the email to the command from a config file, rather than having it fixed in the command.
use App\Console\Commands\SendExpiredLicences;
…
protected function schedule(Schedule $schedule)
{
$recipient = config('myapp.expired.recipient');
$schedule->command(SendExpiredLicences::class, [$recipient])
->weekdays()
->at('08:30');
}
config/myapp.php:
<?php
return [
'expired' => [
'recipient' => 'admin#example.com',
],
];
App\Console\Commands\SendExpiredLicences.php: update the command to accept the email address as an argument, use on-demand notifications, and get rid of preview() method. Neither the command or the notification need to know about the client list, so don't build it yet.
<?php
namespace App\Console\Commands;
use App\Console\Command;
use App\Notifications\NotifyExpiredLicences;
use Illuminate\Support\Facade\Notification;
class SendExpiredLicences extends Command
{
protected $signature = 'email:expired-licences {recipient}';
protected $description = 'Email a list of expired licences to the given address';
public function handle()
{
$recip = $this->argument('recipient');
Notification::route('email', $recip)->notify(new NotifyExpiredLicences());
}
}
App\Notifications\NotifyExpiredLicences.php: the toMail() method should pass the notifiable object (i.e. the user getting notified) along, because the mailable will be responsible for adding the To address before the thing is sent.
<?php
namespace App\Notifications;
use App\Mail\ExpiredLicenses;
use Illuminate\Notifications\Notification;
class NotifyExpiredLicences extends Notification
{
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
return (new ExpiredLicenses($notifiable));
}
}
App\Mail\ExpiredLicences.php: since the mail message actually needs the list of clients, this is where we build it. We get the recipient here, either from the user's email or the anonymous object.
<?php
namespace App\Mail;
use App\Models\Client;
use Illuminate\Notifications\AnonymousNotifiable;
class ExpiredLicences extends Mailable
{
private $email;
public function __construct(private $notifiable)
{
// this allows the notification to be sent to normal users
// not just on-demand
$this->email = $notifiable instanceof AnonymousNotifiable
? $notifiable->routeNotificationFor('mail')
: $notifiable->email;
}
public function build()
{
// or whatever your object is
$clients = Client::whereHas('licenses', fn($q)=>$q->whereExpired(1));
return $this
->subject('Clients with Expired Licences')
->markdown(
'emails.expiredlicences',
['clients' => $clients, 'toname' => $this->notifiable->name ?? 'Admin']
)
->to($this->email);
}
}
For previewing with the browser routes\web.php:
Route::get('/notification', function () {
// create a dummy AnonymousNotifiable object for preview
$anon = Notification::route('email', 'no#example.com');
return (new ExpiredLicencesNotification())
->toMail($anon);
});
I'm working with this case where I need to instantiate an object after a form is submitted in a controller. Everything's working fine until I call this object (as a property) from another method. It appears to be null.
If I intentiate the object from constructor method, I have no problem at all.
I can't keep this object in session because of closure.
Here's what i got so far.
// Version with the object iniate within the constructor that's working
class SearchConsoleController extends Controller
{
private $console;
protected function __construct() {
$callback = route('searchconsole.callback') ;
$this->console = $this->setConsole(env('CLIENT_ID'), env('CLIENT_SECRET'), $callback);
}
private function setConsole($cliendId, $cliendSecret, $callback){
$console = new Console(new Google_Client(), $cliendId, $cliendSecret, $callback);
return $console;
}
public function index(Request $request) {
return view('searchconsole.index')->with('authUrl', $this->console->getAuthUrl());
}
public function callback(Request $request){
if ($request->has('code')) {
$this->console->acceptCode($request->get('code'));
return redirect()->action('SearchConsoleController#listSites', [$request]);
}
else{
die('error');
}
}
Now the version which i'm stucked wih
class SearchConsoleController extends Controller
{
private $console;
private $callback;
protected function __construct() {
$this->callback = route('searchconsole.callback') ;
}
private function setConsole($cliendId, $cliendSecret, $callback){
$console = new Console(new Google_Client(), $cliendId, $cliendSecret, $this->callback);
return $console;
}
public function index(Request $request) {
// VIEW WITH A FORM FROM WHICH I GET CLIENT_SECRET & CLIENT_ID var
return view('searchconsole.index');
}
public function getAuthUrl(Request $request) {
// FORM FROM INDEX IS SUBMITTED
$clientId = ($request->has('google-client-id')) ?
$request->get('google-client-id') :
null
;
$clientSecret = ($request->has('google-client-secret')) ?
$request->get('google-client-secret') :
null
;
$this->console = $this->setConsole($clientId, $clientSecret, $this->callback);
return $this->console->getAuthUrl();
}
public function callback(Request $request){
if ($request->has('code')) {
// ***** MY PROBLEM *********
$this->console->acceptCode($request->get('code')); // HERE $this->console IS NULL;
// *******************
return redirect()->action('SearchConsoleController#listSites', [$request]);
}
else{
die('error');
}
}
I just can't figure out how I can do this so console is still available
UPDATE :
following #iamab.in advice, i looked into Service Provider but i just dont know how i can instante the Console Object within the service provider.
Here's what i've done.
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Helpers\Console;
use Google_Client;
use Illuminate\Support\Facades\Route;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->app->bind(Console::class, function() {
$request = app(\Illuminate\Http\Request::class);
$clientId = ($request->has('google-client-id')) ?
$request->get('google-client-id') :
null
;
$clientSecret = ($request->has('google-client-secret')) ?
$request->get('google-client-secret') :
null
;
$callback = Route::get()->name('searchconsole.callback');
return new Console(new Google_Client(), $clientId, $clientSecret, $callback);
});
}
public function boot(){}
....
I just dont know how and where to implement it.
Thanks again
Update#2 :
okay my solution was working, I just didnt launch the correct app ..... 😅
I am trying to mock a model where I am doing a query on the db. At first I was mocking the where function on the below but then I realised it's actually first that provides the result, however this still doesn't work. I am aware I could just use the database, but our docker setup is super slow and I can't use SQLite as a previous developer created a migration at some point that removes a foreign key.
Test:
protected function setUp(): void
{
parent::setUp();
$this->calendarEventBookingRepository = app(CalendarEventBookingRepository::class);
$this->calendarEventBooking = Mockery::mock(CalendarEventBooking::class);
}
/** #test */
function bookSingleCustomerReturnsNull()
{
$calendarEvent = factory(CalendarEvent::class)->create();
$calendarEventBooking = factory(CalendarEventBooking::class);
$data = new \stdClass();
$data->customer_id = 1;
$this->calendarEventBooking->shouldReceive('first')->once()->andReturn($calendarEventBooking);
$this->app->instance(CalendarEventBooking::class, $this->calendarEventBooking);
$result = $this->calendarEventBookingRepository->bookSingleCustomer($calendarEvent, $data);
$this->assertEquals(null, $result);
}
Function being tested:
public function bookSingleCustomer(CalendarEvent $event, $data)
{
$this->event = $event;
DB::transaction(function () use ($data) {
$alreadyBooked = $this->modelClassName::where([
['customer_id', $data->customer_id]
])->first();
if ($alreadyBooked) {
return null;
}
return "hello";
});
}
Test Output:
Mockery\Exception\InvalidCountException: Method first(<Any Arguments>) from Mockery_0_Models_CalendarEventBooking should be called
exactly 1 times but called 0 times.
I think you should mock the where method as well.
$this->calendarEventBooking->method('where')->willReturnSelf();
To transform a database entity to an API response Laravel support resources, eg. UserResource extends JsonResource. The resource allows me to cleanly define which fields from the entity should be included in the response, how to transform them etc.
Is there a similar functionality for requests? My requests typically look like this:
public function create(JsonRequest $request): UserResource
{
$data = $request->json()->all();
/* Remove, transform, add request fields etc. */
$user = User::create($data);
$user->save();
return new UserResource($user);
}
In our case we have a legacy database behind a modern API so there are a number of fields that need to transformed, renamed etc. before pushing them into the entity class. The fields differ from request to request but the steps are very similar. Is there a less boilerplate-y way to do this, something similar to how resources transform entities to responses?
Something like:
class UserRequest extends JsonRequest {
public function fromArray(JsonRequest $request) {
…
}
}
Then the request could look like this:
public function create(UserRequest $request): UserResource
{
$user = User::create($request);
$user->save();
return new UserResource($user);
}
I suppose, that most of your problems can solve form request. See example below
Form request class:
namespace App\Http\Requests;
use Carbon\Carbon;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
class TestRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'date' => 'required|date_format:Y-m-d H:i:s',
'name' => 'required|string',
];
}
// here you can specify custom error messages
public function messages()
{
return [
'date.required' => 'No date specified',
'date.date_format' => 'Invalid date format',
'name.required' => 'No name specified',
'name.string' => 'Invalid name format',
];
}
// here you can implement some data mapping before validation
protected function validationData()
{
return $this->transform($this->all());
}
// some data transformation logic
// You can place it anywhere in your applciation services
protected function transform($input)
{
$transformed = [];
foreach ($input as $field => $value) {
if ($field == 'name') {
$value = strtoupper($value);
} elseif ($field == 'date') {
$value = Carbon::parse($value)->toDateTimeString();
}
$transformed[$field] = $value;
}
return $transformed;
}
public function failedValidation(Validator $validator)
{
// here you can implement custom validation failure
parent::failedValidation($validator);
}
}
Here is my test route: Route::get('/test', 'TestController#index');
And controller:
use App\Http\Requests\TestRequest;
class TestController extends Controller
{
public function index(TestRequest $request)
{
return response()->json($request->validated());
}
}
So, then requesting route: curl -H 'Accept: application/json' 'http://localhost:8000/test?date=01.01.2019&name=petya'
And getting response: {"date":"2019-01-01 00:00:00","name":"PETYA"}
And dont be shy to see source code of request and form request, cause of not all methods you wish are described in docs. Hope this helps
I have a class like this:
class PostValidator
{
public function __construct(Validator $validator, $data)
{
$this->validator = $validator;
$this->data = $data;
}
}
I read Laravel doc about IoC automatic resolution, it gives an example:
class FooBar {
public function __construct(Baz $baz)
{
$this->baz = $baz;
}
}
$fooBar = App::make('FooBar');
Is it possible to use App::make only without App::bind (with closure) to instantiate my class above which also passing parameter $data?
No, you can't do that.
The idea is that you pass only the dependencies to the constructor, and obviously data is not one. Validator works with the data, but does not depend on the data.
Instead use setter for the data.
class PostValidator
{
public function __construct(Validator $validator)
{
$this->validator = $validator;
}
public function setData($data)
{
$this->data = $data;
}
}
and simply call it explicitly:
$validator = App::make('PostValidator');
$validator->setData($data);
// or in the controller, which is what you're doing most likely
public function __construct(PostValidator $validator)
{
$this->validaotr = $validator;
}
public function update($id)
{
$data = Input::only([ input that you need ]);
$this->validator->setData($data);
// run the validation
...
}
edit: as per comment, this is what 2nd argument $parameters does:
// Foo class with Eloquent Models as dependencies
public function __construct(User $user, Category $category, Post $post)
{
$this->user = $user;
$this->category = $category;
$this->post = $post;
}
then IoC container will resolve the dependencies as newly instantiated models:
$foo = App::make('Foo');
$foo->user; // exists = false
$foo->category; // exists = false
$foo->post; // exists = false
but you can do this if you want:
$user = User::first();
$cat = Category::find($someId);
$foo = App::make('Foo', ['category' => $cat, 'user' => $user]);
$foo->user; // exists = true, instance you provided
$foo->category; // exists = true, instance you provided
$foo->post; // exists = false, newly instantiated like before