#Retrieve raw value of an attribute in Eloquent model before applied cast

3 messages · Page 1 of 1 (latest)

hearty jetty
#

I am trying to retrieve the raw value of an attribute in an Eloquent model on saving event, before any casts are applied. Tried this with Laravel's built-in hashed cast (see method in trait HasAttributes::setAttribute() which calls HasAttributes::castAttributeAsHashedString()) and with my own cast:

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;

class Hashed implements CastsInboundAttributes
{
    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return $value !== null && password_get_info($value)['algo'] === null ? Hash::make($value) : $value;
    }
}

then, in the model, I try to retrieve the original unhashed value with this trait:

<?php

namespace App\Models\Traits;

use Illuminate\Database\Eloquent\Model;

/** @mixin Model */
trait HasPassword
{
    public static function bootHasPassword(): void
    {
        static::saving(function (self $model) {
            if ($model->isDirty('password')) {
                $model->password_changed_at = now();
            }
            dump($model->getRawOriginal('password'));
            // ... and do some more fancy things with the plaintext password, e.g. zxcvbn password strength calculation
        });
    }
}

but getRawOriginal() returns the already hashed value, no matter if I use 'hashed' cast (Laravel built-in) or my own implementation App\Casts\Hashed (which I thought would get applied later then HasAttributes::setAttribute()). How could this be done without using a mutator as a workaround?

iron hearth
#

Why do you want to avoid using a mutator? You could override the setAttribute in your model instead of using casts, but I would not recommend doing this.

hearty jetty
# iron hearth Why do you want to avoid using a mutator? You could override the setAttribute in...

Hi, thanks for your response. I would have preferred just using Laravel's built-in hashed cast, as it's already there and I always try to stick with the framework defaults as much as I can. If I use my own mutator, I also need to care about that logic (which just checks if the value is already hashed).

I ended up with this solution:

<?php

namespace App\Models\Traits;

use App\Helpers\Password;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;

/** @mixin Model */
trait HasPassword
{
    public function initializeHasPassword(): void
    {
        $this->hidden = Arr::mergeUnique($this->hidden, [
            'password',
            'password_score',
            'password_changed_at',
        ]);
        $this->casts = array_merge($this->casts, [
            'password'            => 'hashed', // NOTE: this is superfluous and already done in mutator below
            'password_changed_at' => 'datetime',
        ]);
    }

    public function username(): string
    {
        return $this->username;
    }

    /**
     * Stores the password as bcrypt hash and calculates Zxcvbn score (0-4) if it is changed.
     * Doing this in a mutator instead of a cast (see Laravel built-in 'hashed' cast) so we can retrieve the original
     * plaintext password for Zxcvbn score calculation.
     */
    protected function password(): Attribute
    {
        return Attribute::make(
            set: fn (#[\SensitiveParameter] string $value) => [
                'password'            => Password::hash($value),
                'password_score'      => Password::score($value, [$this->username()]),
                'password_changed_at' => $this->exists ? now() : null,
            ],
        );
    }
}