Phone Attribute and sendPasswordReset notification voodoo

I have this on my user model, this is super weird. In all cases, the user is created just fine, but if i try modify the phone number, the password reset email fails to send. As mentioned, in all cases, the user is created fine.
protected function phone(): Attribute
{
return Attribute::make(
get: function ($value) {
// Format the phone number as '555-555-5555' if the value is set
return $value ? substr($value, 0, 3) . '-' . substr($value, 3, 3) . '-' . substr($value, 6) : null;
},
set: function ($value) {
if (blank($value)) {
return;
}

ray($value); // Debugging line: 555-555-5555
// Remove any non-numeric characters
// $value = preg_replace('/[^0-9]/', '', $value); // Fails to send password reset email
// $value = '5555555555'; // Fails to send password reset email
$value = '555-555-5555'; // Works to send password reset email

// Remove leading 1 if it's an 11-digit number
if (strlen($value) === 11 && Str::startsWith($value, '1')) {
$value = Str::substr($value, 1);
}

return $value;
}
);
}
protected function phone(): Attribute
{
return Attribute::make(
get: function ($value) {
// Format the phone number as '555-555-5555' if the value is set
return $value ? substr($value, 0, 3) . '-' . substr($value, 3, 3) . '-' . substr($value, 6) : null;
},
set: function ($value) {
if (blank($value)) {
return;
}

ray($value); // Debugging line: 555-555-5555
// Remove any non-numeric characters
// $value = preg_replace('/[^0-9]/', '', $value); // Fails to send password reset email
// $value = '5555555555'; // Fails to send password reset email
$value = '555-555-5555'; // Works to send password reset email

// Remove leading 1 if it's an 11-digit number
if (strlen($value) === 11 && Str::startsWith($value, '1')) {
$value = Str::substr($value, 1);
}

return $value;
}
);
}
Solution:
@wyChoong modified the password reset method to ```php class CreateUser extends CreateRecord { protected static string $resource = UserResource::class; ...
Jump to solution
7 Replies
Mark Chaney
Mark Chaney5mo ago
This is what the user creation is like https://gist.github.com/MACscr/89fd2f4ca6a7f132862ea5fd139270ed. I dont think its filament specific of an issue, but I am a bit dumbfounded. $data and $user look complete in the handleRecordCreation() method as well i really dont know what magic is being used in sendPasswordReset email though. lol. That sendPasswordReset method seems to send a notification that says "we cant find a user with that email address". This is at the same time it sends a notification that the user was created successfully, which it was. I have a pest test fails to verify that the password reset was sent. If comment it out like i listed above, it passes
test('after creating a new user a reset-password email is sent', function () {
Notification::fake();

$email = fake()->email();

$this->get(UserResource::getUrl('create'))->assertSuccessful();

livewire(CreateUser::class)
->fillForm([
'name' => fake()->name(),
'email' => $email,
'title' => fake()->jobTitle(),
'phone' => '555-555-5555',
])
->call('create')
->assertHasNoFormErrors();

$user = User::firstWhere('email', $email);

Notification::assertSentTo($user, ResetPasswordNotification::class);
});
test('after creating a new user a reset-password email is sent', function () {
Notification::fake();

$email = fake()->email();

$this->get(UserResource::getUrl('create'))->assertSuccessful();

livewire(CreateUser::class)
->fillForm([
'name' => fake()->name(),
'email' => $email,
'title' => fake()->jobTitle(),
'phone' => '555-555-5555',
])
->call('create')
->assertHasNoFormErrors();

$user = User::firstWhere('email', $email);

Notification::assertSentTo($user, ResetPasswordNotification::class);
});
. 1) all user data looks fine and is created correctly 2) why would this reset notification even care about the phone field and its format?. its just super odd. the password reset method in the test is just use Filament\Notifications\Auth\ResetPassword as ResetPasswordNotification;
wyChoong
wyChoong5mo ago
1. can you test the user create + notifiation without filament/livewire 2. what filament version are you on, could it be DB transaction?
Mark Chaney
Mark Chaney5mo ago
@wyChoong Running latest filament. Now i just noticed in the mutator, if i set the phone number to 554-555-5555, it fails then too. So im thinking it might have something to do with livewire and serialization? I bet if i used a mutator on the 'name', i would run into the same issue actually messign with name like so
protected function name(): Attribute
{
return Attribute::make(
set: fn (string $value) => strtoupper($value),
);
}
protected function name(): Attribute
{
return Attribute::make(
set: fn (string $value) => strtoupper($value),
);
}
didnt cause any problems though
protected function name(): Attribute
{
return Attribute::make(
set: fn (string $value) => 'Frank',
);
}
protected function name(): Attribute
{
return Attribute::make(
set: fn (string $value) => 'Frank',
);
}
does fail to send the password reset if the original name was 'bill'
Solution
Mark Chaney
Mark Chaney5mo ago
@wyChoong modified the password reset method to
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;

protected function handleRecordCreation(array $data): Model
{
$data['password'] = bcrypt(Str::random());

$user = parent::handleRecordCreation($data);

$this->sendPasswordResetEmail($user);

return $user;
}

private function sendPasswordResetEmail(Model $user): void
{
$status = Password::broker(Filament::getAuthPasswordBroker())->sendResetLink(
['email' => $user->email],
function (CanResetPassword $user, string $token): void {
if (! method_exists($user, 'notify')) {
$userClass = get_class($user);
throw new Exception("Model [{$userClass}] does not have a [notify()] method.");
}

$notification = new ResetPasswordNotification($token);
$notification->url = Filament::getResetPasswordUrl($token, $user);

$user->notify($notification);
},
);

if ($status !== Password::RESET_LINK_SENT) {
Notification::make()
->title(__($status))
->danger()
->send();
} else {
Notification::make()
->title(__($status))
->success()
->send();
}
}
}
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;

protected function handleRecordCreation(array $data): Model
{
$data['password'] = bcrypt(Str::random());

$user = parent::handleRecordCreation($data);

$this->sendPasswordResetEmail($user);

return $user;
}

private function sendPasswordResetEmail(Model $user): void
{
$status = Password::broker(Filament::getAuthPasswordBroker())->sendResetLink(
['email' => $user->email],
function (CanResetPassword $user, string $token): void {
if (! method_exists($user, 'notify')) {
$userClass = get_class($user);
throw new Exception("Model [{$userClass}] does not have a [notify()] method.");
}

$notification = new ResetPasswordNotification($token);
$notification->url = Filament::getResetPasswordUrl($token, $user);

$user->notify($notification);
},
);

if ($status !== Password::RESET_LINK_SENT) {
Notification::make()
->title(__($status))
->danger()
->send();
} else {
Notification::make()
->title(__($status))
->success()
->send();
}
}
}
and it does the job
wyChoong
wyChoong5mo ago
That’s funny The change was $data to $user only right?
Mark Chaney
Mark Chaney5mo ago
No description
Mark Chaney
Mark Chaney5mo ago
hmm, I thought i had a good explanation from chatgpt why it was causing problems before, but i cant seem to locate it. spent enough time on it already. lol