Does Laravel 6 disable observers in factories / tests? - laravel

Ive just written an observer thats sends an e-mail whenever a user is created.
class UserObserver
{
public function created(User $user)
{
Mail::to($user)->send(new UserAccountCreated(
app('auth.password.broker')->createToken($user),
$user
));
}
}
I ran phpunit to test if my observer works, and it passed. However I was expecting to get an email for each time my tests create a user.
For example:
/** #test */
public function an_admin_can_view_all_clients()
{
$user = factory(User::class)->create(['is_admin' => true]);
$client = factory(Client::class)->create();
$client2 = factory(Client::class)->create();
$this->actingAs($user)->get(route('clients.index'))
->assertSuccessful()
->assertSee($client->name)
->assertSee($client2->name);
}
I would expect an email to be sent when that factory creates the user. But I don't receive one in Mailtrap.
Just wondering if and where laravel disables my observer being triggered when my factory creates that user.

No you have to disable it yourself by using Model::withoutEvents()
For example:
$user = User::first();
User::withoutEvents(function () use ($user) {
$user->delete();
});
Also in this specific case you can also use the Mail fake system provided by Laravel itself

Related

How do I use a policy on an index that doesn't use the model the policy belongs to?

What I am trying to do is apply a policy on a control method that lists a bunch of records instead of just one record like most of the examples I have seen.
Instead of checking against the ThoughtRecords I want to check the signed in user hashedId to the user that's being queried hashedId in the controller index() method.
Apparently in the Laravel docs the model class needs to be passed on actions that don't require a model. So I'm confused how to make this work.
AuthServiceProvider.php
protected $policies = [
'App\ThoughtRecord' => 'App\Policies\ThoughtRecordPolicy',
];
public function boot()
{
$this->registerPolicies();
}
ThoughtRecordPolicy.php
public function view(User $signedInUser, User $client)
{
//return true;
dd('Policy working');
//return $signedInUser->id === $client->id;
}
ThoughtRecordController.php
public function index($userHashedId)
{
$client = User::where('hashed_id', $userHashedId)->first();
$this->authorize('view', ThoughtRecord::class, $client);
$records = ThoughtRecord::where('user_id', $client->id)->latest()->paginate(1);
return ThoughtRecordResource::collection($records);
}
Error
Too few arguments to function App\Policies\ThoughtRecordPolicy::view()
I have also tried:
$this->authorize('view', $client);
This action is unauthorized.
As said:
Apparently in the Laravel docs the model class needs to be passed on actions that don't require a model. So I'm confused how to make this work.
You need pass both the ThoughtRecord::class and the $client into an array:
$this->authorize('view', [ThoughtRecord::class, $client]);

Call to undefined method Illuminate\Database\Eloquent\Relations\BelongsToMany::routeNotificationFor()

I'm building a messaging system that notifies each user in the conversation when a reply is set.
MessageNotification.php
class MessageNotification extends Notification
{
use Queueable;
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['database'];
}
public function toArray($notifiable)
{
return [
'data' => 'Messenger notification'
];
}
}
InboxController
public function reply($hashedId, Request $request)
{
$this->validate($request, [
'body' => 'required',
]);
$conversation = Conversation::where('hashed_id', $hashedId)->first();
$users = $conversation->participants();
//dd($conversationUserIds);
$notifications = Notification::send($users, new MessageNotification());
$message = $conversation->messages()->create([
'sender_id' => auth()->user()->id,
'body' => $request->body,
]);
return new MessageResource($message);
}
Error
Call to undefined method Illuminate\Database\Eloquent\Relations\BelongsToMany::routeNotificationFor()
Extra Information
I've had to build a custom Notifiable trait due to needing to use both Laravel Sparks notification system and Laravels stock notification system. Tutorial I got code from.
Custom notification trait
namespace App\Traits;
use Illuminate\Notifications\Notifiable as BaseNotifiable;
use App\Notifications\DatabaseNotification;
trait Notifiable {
use BaseNotifiable;
public function notifications() {
return $this->morphMany(DatabaseNotification::class, 'notifiable')->orderBy('created_at', 'desc');
}
}
Also note that $reciever->notify(new MessageNotification()); works just fine when sending a notification to one user. The only other solution I saw on this was: https://laracasts.com/discuss/channels/code-review/call-to-undefined-method-routenotificationfor-while-sending-email-to-multiple-users
I tried to implement that, but I'm using a database channel so it shouldn't make a difference.
This line here:
$users = $conversation->participants();
Will set the $users variable to a QueryBuilder instance (assuming you are using conventional Laravel relationships), rather than a collection of users. This is because the () at the end of a relationship builds the query but doesn't run it yet. So then when you call Notification::send($users, etc...) you are not passing in a collection of users; you are passing in a QueryBuilder object.
Try this instead:
$users = $conversation->participants;
Again - this is assuming that the participants method on the Conversation model is a standard laravel relationship.

Call to a member function notify() on integer in LARAVEL 5.8

I am working on a task schedule. The flow is I need to send notification as a reminder for their duedate.
here's my console\command\remindDuedate
class remindDuedate extends Command
{
protected $signature = 'remindDuedate:run';
protected $description = 'Command description';
public function __construct()
{
parent::__construct();
}
public function handle()
{
while (true) {
$loanapplications = LoanApplication::where('archive',false)->where('status','=',2)->get();
foreach ($loanapplications as $application) {
$user = $application->user_id;
$date_approval = Carbon::createFromTimestamp(strtotime($application->date_approval));
$duration = $application->loanDuration->num_days;
$duedate_warning = $duration-3;
$reminder_date = $date_approval->addDays($duedate_warning)->toDateString();
$now = Carbon::now('Asia/Manila')->toDateString();
$duedate = Carbon::now('Asia/Manila')->addDays(3)->toDateString();
if($reminder_date == $now) {
$user->notify(new remindDuedateNotif());
}
}
}
}
}
php artisan remindDuedate:run
remindDuedateNotif
Why do I getting the "Call to a member function notify() on integer"
Thank you in advance!
You are not fetching the user, therefor the user is still an integer, set it like so.
$user = User::find($application->user_id);
EDIT
As you can see your notification takes a user as the first argument. Therefor send it with it.
$user->notify(new remindDuedateNotif($user));
This is weird to send and notify on the User object and pass it. You are in luck as every $notifiable parameter is actually an user, as it would be the object you send it from.
So remove $user from __contruct() and everywhere you access the user, you can do the following.
'user_id' => $notifiable->id,
1) the User Model should have the notifiable trait
Illuminate\Notifications\Notifiable
2) you need to add a relation between the application and user in thee application model
public function user()
{
return $this->belongsTo(User::class);
}
3) notify the application user :
$application->user->notify(new remindDuedateNotif());

How to Verify Email Without Asking the User to Login to Laravel

I am developing a Laravel application. My application is using Laravel built-in auth feature. In the Laravel auth when a user registers, a verification email is sent. When a user verifies the email click on the link inside the email, the user has to login again to confirm the email if the user is not already logged in.
VerificationController
class VerificationController extends Controller
{
use VerifiesEmails, RedirectsUsersBasedOnRoles;
/**
* Create a new controller instance.
* #return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
public function redirectPath()
{
return $this->getRedirectTo(Auth::guard()->user());
}
}
I tried commenting on this line.
$this->middleware('auth');
But it's s not working and instead, throwing an error. How can I enable Laravel to be able to verify email even if the user is not logged in?
First, remove the line $this->middleware('auth');, like you did.
Next, copy the verify method from the VerifiesEmails trait to your VerificationController and change it up a bit. The method should look like this:
public function verify(Request $request)
{
$user = User::find($request->route('id'));
if (!hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($user->markEmailAsVerified())
event(new Verified($user));
return redirect($this->redirectPath())->with('verified', true);
}
This overrides the method in the VerifiesUsers trait and removes the authorization check.
Security (correct me if I'm wrong!)
It's still secure, as the request is signed and verified. Someone could verify another user's email address if they somehow gain access to the verification email, but in 99% of cases this is hardly a risk at all.
Here's a more future proof solution to the problem:
class VerificationController extends Controller
{
// …
use VerifiesEmails {
verify as originalVerify;
}
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth'); // DON'T REMOVE THIS
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
/**
* Mark the authenticated user's email address as verified.
*
* #param Request $request
* #return Response
*
* #throws AuthorizationException
*/
public function verify(Request $request)
{
$request->setUserResolver(function () use ($request) {
return User::findOrFail($request->route('id'));
});
return $this->originalVerify($request);
}
}
So when an email confirmation link is clicked by an unauthenticated user the following will happen:
User will be redirected to the login view 1
User enters credentials; logs in successfully 2
User will be redirect back to the email confirmation URL
Email will be marked as confirmed
1 The email will not be marked as confirmed at this point.
2 The user may enter bad credentials multiple times. As soon as he enters the correct credentials he will be redirected to the intended email confirmation URL.
// For Laravel 6 and Above
use Illuminate\Auth\Events\Verified;
use Illuminate\Http\Request;
use App\User;
// comment auth middleware
//$this->middleware('auth');
public function verify(Request $request)
{
$user = User::find($request->route('id'));
if (!hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($user->markEmailAsVerified())
event(new Verified($user));
return redirect($this->redirectPath())->with('verified', true);
}
Solution to allow email verification for users who are not logged in (i.e. without auth):
Changes to: app/Http/Controllers/Auth/VerificationController.php:
$this->middleware('auth'); to $this->middleware('auth')->except('verify');
Copy verify() method from the VerifiesEmails trait.
Edit verify method to work without expected $request->user() data.
My verify() method in the VerificationController looks like this:
public function verify(\Illuminate\Http\Request $request)
{
$user = User::find($request->route('id'));
if ($request->route('id') != $user->getKey()) {
throw new AuthorizationException;
}
if ($user->markEmailAsVerified())
event(new Verified($user));
return redirect()->route('login')->with('verified', true);
}
Signed middleware
Laravel uses a middleware named signed to check the integrity of URLs that were generated by the application. Signed checks whether the URL has been changed since it was created. Try changing the id, expiry time or the signature in the url and it will lead to an error - very effective and useful middleware to protect the verify() method
For more information: https://laravel.com/docs/8.x/urls#signed-urls
(Optional)
I redirected my users to the login route, rather than the intended route for two reasons. 1) After login, it would try to redirect the user to the email verification link, leading to an error; 2) I wanted to use the verified true flash data that was attached to the redirect, to show an alert on the login page, if the user had successfully verified their email address.
Example of my login page alert:
#if(session()->has('verified'))
<div class="alert alert-success">Your email address has been successfully verified.</div>
#endif
Suggestions
If you have any suggestions on how I could improve this code, please let me know. I'd be happy to edit this answer.
You should not remove $this->middleware('auth') altogether as that will effect the redirects. If you remove it, the unauthenticated users will be redirected to "/email/verify" instead of "/login"
so $this->middleware('auth'); will be changed to $this->middleware('auth')->except('verify'); in "VerificationController"
Also copy the "verify" function from "VerifiesEmails" into "VerificationController"
add these two lines of code at the top of the function
$user = User::find($request->route('id'));
auth()->login($user);
so you are logging in the user programmatically and then performing further actions
Here's my take on the situation. Verification requires user to login before it can complete the verification, so we can override the verify function and login user using ID we received in the link. It is safe cause verify function is not called if Laravel can't verify the signature from URL so even if someone temper the URL they won't be able to bypass it.
Go to your VerificationController and add the following function at the end of the file.
public function verify(Request $request)
{
if (!auth()->check()) {
auth()->loginUsingId($request->route('id'));
}
if ($request->route('id') != $request->user()->getKey()) {
throw new AuthorizationException;
}
if ($request->user()->hasVerifiedEmail()) {
return redirect($this->redirectPath());
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect($this->redirectPath())->with('verified', true);
}
Note
Make sure you have same_site value in 'config/session.php' set to 'lax'. If it is set to 'strict' then it won't persist session if you were redirected from another site. For example, if you click a verification link from Gmail then your session cookie won't persist, so it won't redirect you to dashboard, but it sets 'email_verified_at' field in the database marking the verification successful. The user won't get any idea what was happened because it will redirect the user to the login page. When you have set it to 'strict', it will work if you copy the verification link directly in the browser address bar but not if the user clicks the link from the Gmail web client because it uses redirect to track the link.
if you want to active user account without login you can do that in 2 steps
1- Remove or comment Auth middleware in VerificationController
Example below:
public function __construct()
{
//$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
2- since verify route passing the {id} you can just edit verify function to find the user by the route id request like code below :
file path : *:\yourproject\vendor\laravel\framework\src\Illuminate\Foundation\Auth\VerifiesEmails.php
$user = User::findOrfail($request->route('id'));
Complete example
public function verify(Request $request)
{
$user = User::findOrfail($request->route('id'));
if (! hash_equals((string) $request->route('id'), (string) $user->getKey())) {
throw new AuthorizationException;
}
if (! hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($user->hasVerifiedEmail()) {
return redirect($this->redirectPath())->with('verified', true);
}
if ($user->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect($this->redirectPath())->with('registered', true);
}
I change EmailVerificationRequest but i now this is wrong, any way it's work.
Warning
This change on the vendor
protected $user;
public function authorize()
{
$this->user = \App\Models\User::find($this->route('id'));
if ($this->user != null){
if (! hash_equals((string) $this->route('id'),
(string) $this->user->getKey())) {
return false;
}
if (! hash_equals((string) $this->route('hash'),
sha1($this->user->getEmailForVerification()))) {
return false;
}
return true;
}
return false;
}
To use inner laravel logic (without overriding the logic), we simply create $request->user() and call trait's verify method. And manually sign in the user when the verification is successful.
use VerifiesEmails {
verify as parentVerify;
}
public function verify(Request $request)
{
$user = User::find($request->route('id'));
if (!$user) return abort(404);
$request->setUserResolver(function () use($user) {
return $user;
});
return $this->parentVerify($request);
}
public function verified(Request $request)
{
Auth::login($request->user());
}

Laravel 5.3 Middleware Request Merge Not Return Any Value

All my code is working on Laravel 5.2. Now I try to upgrade to Laravel 5.3 it breaks on middleware.
// Verify Middleware
public function handle($request, Closure $next)
{
// I already make sure the data is exists
$user = User::find('abc');
if (!$user) {
return responseHandler()->unauthorized('Unauthorized');
}
$request->merge(['user_id' => $user->id, 'device' => $device]);
return $next($request);
}
// User Controller
public function __construct(Request $request)
{
var_dump($request->all());
$this->user_id = $request->user_id;
$this->device = $request->device;
}
public function getProfile(Request $request)
{
$data = User::find($this->user_id);
$result = Fractal::item($data, new UserTransformer)->getArray();
return responseHandler()->success(0, $result, 'user');
}
The problem is $this->user_id is always null. But if I request from getProfile function it return correctly.
var_dump result only the login info. it not received any merge request from middleware
On Laravel 5.2 this code is working properly. Any solution?.
Reference-: https://www.laravel.com/docs/5.3/upgrade#upgrade-5.3.0
Session In The Constructor
In previous versions of Laravel, you could access session variables or the authenticated user in your controller's constructor. This was never intended to be an explicit feature of the framework. In Laravel 5.3, you can't access the session or authenticated user in your controller's constructor because the middleware has not run yet.
As an alternative, you may define a Closure based middleware directly in your controller's constructor. Before using this feature, make sure that your application is running Laravel 5.3.4 or above:
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
class ProjectController extends Controller
{
/**
* All of the current user's projects.
*/
protected $projects;
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->projects = Auth::user()->projects;
return $next($request);
});
}
}
Of course, you may also access the request session data or authenticated user by type-hinting the Illuminate\Http\Request class on your controller action:
It seems you can't access the session data in Controller constructor because the middleware not run yet.
Refer laravel change log here
you can't access the session or authenticated user in your
controller's constructor because the middleware has not run yet.
In your case you can call session with request object
public function getProfile(Request $request)
{
$data = User::find($request->session()->get('user_id'));
$result = Fractal::item($data, new UserTransformer)->getArray();
return responseHandler()->success(0, $result, 'user');
}

Resources