Framework: originalIsEquivalent ignores Custom Casts

Created on 16 Jul 2020  路  7Comments  路  Source: laravel/framework


  • Laravel Version: 7.20.0
  • PHP Version: 7.4.5
  • Database Driver & Version: SQLServer

Description:

When trying to retrive dirty Fields from an Eloquent Model, which uses the new introduced Custom Cast-Mutator the called method originalIsEquivalent ignores the Custom Cast. This results in an always dirty attribute, even if the orginal and the new Attribute should be equal.
I am not sure why the originalIsEquivalent methodes checks only for primitive Cast Types, maybe this behavior is on purpose?

public function originalIsEquivalent($key)
    {
        if (! array_key_exists($key, $this->original)) {
            return false;
        }

        $attribute = Arr::get($this->attributes, $key);
        $original = Arr::get($this->original, $key);

        if ($attribute === $original) {
            return true;
        } elseif (is_null($attribute)) {
            return false;
        } elseif ($this->isDateAttribute($key)) {
            return $this->fromDateTime($attribute) ===
                   $this->fromDateTime($original);
        } elseif ($this->hasCast($key, ['object', 'collection'])) {
            return $this->castAttribute($key, $attribute) ==
                $this->castAttribute($key, $original);
        } elseif ($this->hasCast($key, ['real', 'float', 'double'])) {
            if (($attribute === null && $original !== null) || ($attribute !== null && $original === null)) {
                return false;
            }

            return abs($this->castAttribute($key, $attribute) - $this->castAttribute($key, $original)) < PHP_FLOAT_EPSILON * 4;
        } elseif ($this->hasCast($key, static::$primitiveCastTypes)) {  //--------------Edited----------------
            return $this->castAttribute($key, $attribute) ===
                   $this->castAttribute($key, $original);
        }

        return is_numeric($attribute) && is_numeric($original)
               && strcmp((string) $attribute, (string) $original) === 0;
    }

My quick fix was to override this method in my model which used the costum class and removed the types from the hasCast method call: $this->hasCast($key). Which resulted in the expected behavior.
Maybe someone can help if this is a bug?

needs more info

Most helpful comment

Should support be added for an optional method on the custom cast to check itself for equivalency?

All 7 comments

Bonjour je suis debutant un conseil pour mes projets Laravel qui qui me signalent des erreurs introuvables

@Babou246 S'il vous pla卯t essayer un forum comme Laracasts et Laravel.io, ou Discord. Et de pr茅f茅rence en anglais, merci.

Share your model and custom cast class please.

Sure:

I added a birthday column to the user model:

  Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->dateTime('birthday');
            $table->rememberToken();
            $table->timestamps();
        });

(I know DateTime doesn麓t make sence in this context. But i am bound to an already existing database in my real project, which uses dateTime for dates)

This is the model:

<?php

namespace App;

use App\Casts\DateString;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 'birthday'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'birthday' => DateString::class
    ];
}

And this is the cast:

?php

namespace App\Casts;

use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class DateString implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param string $key
     * @param mixed $value
     * @param array $attributes
     * @return string
     * @throws \Exception
     */
    public function get($model, $key, $value, $attributes)
    {
        return $value ? (new Carbon($value))->format("Y-m-d") : null;
    }

    /**
     * Prepare the given value for storage.
     *
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param string $key
     * @param mixed $value
     * @param array $attributes
     * @return mixed
     * @throws \Exception
     */
    public function set($model, $key, $value, $attributes)
    {
        if (is_string($value))
            return $value;
        else
            return (new Carbon($value))->format("Y-m-d");
    }
}
````

This is the result.

$user = factory(User::class)->create()
$user->refresh()
$user->birthday
=> "2020-01-01"
$user->getDirty()
=> []
$user->birthday = "2020-01-01"
=> "2020-01-01"
$user->getDirty()
=> [
"birthday" => "2020-01-01",
]

By the way. Is there a better way to handle Dates which have no concept of timezones? Problem is, if I use the default date Mutator, the result which comes out of my resource api is:

{
data: {
...
birthday: "2019-12-31T23:00:00.000000Z"
...
}
```
Which is a little bit annyoing, when trying to add this value to an date Input

Can you also share the code that reproduces the problem?

By the way. Is there a better way to handle Dates which have no concept of timezones?

You are responsible for this yourself. Save your datetimes in UTC and save the timezone in a separate column if you need it.

This is currently the intended behavior. It is difficult to know if a custom cast object is equivalent.

Should support be added for an optional method on the custom cast to check itself for equivalency?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lzp819739483 picture lzp819739483  路  3Comments

JamborJan picture JamborJan  路  3Comments

Anahkiasen picture Anahkiasen  路  3Comments

progmars picture progmars  路  3Comments

Fuzzyma picture Fuzzyma  路  3Comments