diff --git a/src/Illuminate/Contracts/Database/Eloquent/ComparesCastableAttributes.php b/src/Illuminate/Contracts/Database/Eloquent/ComparesCastableAttributes.php new file mode 100644 index 000000000000..5c9ad195b763 --- /dev/null +++ b/src/Illuminate/Contracts/Database/Eloquent/ComparesCastableAttributes.php @@ -0,0 +1,19 @@ +resolveCasterClass($key)->compare( + $this, $key, $original, $value + ); + } + /** * Determine if the cast type is a custom date time cast. * @@ -1800,6 +1815,19 @@ protected function isClassSerializable($key) method_exists($this->resolveCasterClass($key), 'serialize'); } + /** + * Determine if the key is comparable using a custom class. + * + * @param string $key + * @return bool + */ + protected function isClassComparable($key) + { + return ! $this->isEnumCastable($key) && + $this->isClassCastable($key) && + method_exists($this->resolveCasterClass($key), 'compare'); + } + /** * Resolve the custom caster class for a given key. * @@ -2265,6 +2293,8 @@ public function originalIsEquivalent($key) } return false; + } elseif ($this->isClassComparable($key)) { + return $this->compareClassCastableAttribute($key, $original, $attribute); } return is_numeric($attribute) && is_numeric($original) diff --git a/tests/Database/EloquentModelCustomCastingTest.php b/tests/Database/EloquentModelCustomCastingTest.php index c8487d5d1b95..a3f77227ecb6 100644 --- a/tests/Database/EloquentModelCustomCastingTest.php +++ b/tests/Database/EloquentModelCustomCastingTest.php @@ -6,6 +6,7 @@ use GMP; use Illuminate\Contracts\Database\Eloquent\Castable; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; +use Illuminate\Contracts\Database\Eloquent\ComparesCastableAttributes; use Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Eloquent\Model; @@ -53,6 +54,11 @@ public function createSchema() $table->increments('id'); $table->decimal('amount', 4, 2); }); + + $this->schema()->create('documents', function (Blueprint $table) { + $table->increments('id'); + $table->json('document'); + }); } /** @@ -64,6 +70,7 @@ protected function tearDown(): void { $this->schema()->drop('casting_table'); $this->schema()->drop('members'); + $this->schema()->drop('documents'); } #[RequiresPhpExtension('gmp')] @@ -176,6 +183,25 @@ public function testModelWithCustomCastsWorkWithCustomIncrementDecrement() $this->assertEquals('3.00', $model->amount->value); } + public function test_model_with_custom_casts_compare_function() + { + // Set raw attribute, this is an example of how we would receive JSON string from the database. + // Note the spaces after the colon. + $model = new Document(); + $model->setRawAttributes(['document' => '{"content": "content", "title": "hello world"}']); + $model->save(); + + // Inverse title and content this would result in a different JSON string when json_encode is used + $document = new \stdClass(); + $document->title = 'hello world'; + $document->content = 'content'; + $model->document = $document; + + $this->assertFalse($model->isDirty('document')); + $document->title = 'hello world 2'; + $this->assertTrue($model->isDirty('document')); + } + /** * Get a database connection instance. * @@ -410,3 +436,30 @@ class Member extends Model 'amount' => Euro::class, ]; } + +class Document extends Model +{ + public $timestamps = false; + + protected $casts = [ + 'document' => StructuredDocumentCaster::class, + ]; +} + +class StructuredDocumentCaster implements CastsAttributes, ComparesCastableAttributes +{ + public function get($model, $key, $value, $attributes) + { + return json_decode($value); + } + + public function set($model, $key, $value, $attributes) + { + return json_encode($value); + } + + public function compare($model, $key, $value1, $value2) + { + return json_decode($value1) == json_decode($value2); + } +}