Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/database/src/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ trait HasAttributes
*/
protected array $attributeCastCache = [];

/**
* The cached merged casts array for this instance.
*/
protected ?array $mergedCastsCache = null;

/**
* The built-in, primitive cast types supported by Eloquent.
*
Expand Down Expand Up @@ -193,6 +198,8 @@ protected function initializeHasAttributes(): void
$this->casts = $this->ensureCastsAreStringValues(
array_merge($this->casts, $this->casts()),
);

$this->mergedCastsCache = null;
}

/**
Expand Down Expand Up @@ -711,6 +718,8 @@ public function mergeCasts(array $casts): static

$this->casts = array_merge($this->casts, $casts);

$this->mergedCastsCache = null;

return $this;
}

Expand Down Expand Up @@ -1500,7 +1509,10 @@ public function hasCast(string $key, array|string|null $types = null): bool
public function getCasts(): array
{
if ($this->getIncrementing()) {
return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
return $this->mergedCastsCache ??= array_merge(
[$this->getKeyName() => $this->getKeyType()],
$this->casts,
);
}

return $this->casts;
Expand Down
7 changes: 7 additions & 0 deletions src/database/src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,8 @@ public function setKeyName(string $key): static
{
$this->primaryKey = $key;

$this->mergedCastsCache = null;

return $this;
}

Expand All @@ -1891,6 +1893,8 @@ public function setKeyType(string $type): static
{
$this->keyType = $type;

$this->mergedCastsCache = null;

return $this;
}

Expand All @@ -1909,6 +1913,8 @@ public function setIncrementing(bool $value): static
{
$this->incrementing = $value;

$this->mergedCastsCache = null;

return $this;
}

Expand Down Expand Up @@ -2308,6 +2314,7 @@ public function __sleep(): array

$this->classCastCache = [];
$this->attributeCastCache = [];
$this->mergedCastsCache = null;
$this->relationAutoloadCallback = null;
$this->relationAutoloadContext = null;

Expand Down
2 changes: 2 additions & 0 deletions src/database/src/Eloquent/SoftDeletes.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public function initializeSoftDeletes(): void
if (! isset($this->casts[$this->getDeletedAtColumn()])) {
$this->casts[$this->getDeletedAtColumn()] = 'datetime';
}

$this->mergedCastsCache = null;
}

/**
Expand Down
164 changes: 164 additions & 0 deletions tests/Database/DatabaseEloquentModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@
use Hypervel\Database\Eloquent\Model;
use Hypervel\Database\Eloquent\Relations\BelongsTo;
use Hypervel\Database\Eloquent\Relations\Relation;
use Hypervel\Database\Eloquent\SoftDeletes;
use Hypervel\Database\Query\Builder as BaseBuilder;
use Hypervel\Database\Query\Grammars\Grammar;
use Hypervel\Database\Query\Processors\Processor;
use Hypervel\Events\Dispatcher as EventDispatcher;
use Hypervel\Support\Carbon;
use Hypervel\Support\Collection as BaseCollection;
use Hypervel\Support\Fluent;
Expand Down Expand Up @@ -2904,6 +2906,151 @@ public function testMergeCastsMergesCasts()
$this->assertArrayHasKey('foo', $model->getCasts());
}

public function testGetCastsCacheIsInvalidatedByMergeCasts()
{
$model = new ModelStub;

$before = $model->getCasts();
$this->assertArrayNotHasKey('foo', $before);

$model->mergeCasts(['foo' => 'date']);

$after = $model->getCasts();
$this->assertArrayHasKey('foo', $after);
$this->assertSame('date', $after['foo']);
}

public function testGetCastsCacheIsInvalidatedBySetKeyName()
{
$model = new ModelStub;

$before = $model->getCasts();
$this->assertArrayHasKey('id', $before);

$model->setKeyName('uuid');

$after = $model->getCasts();
$this->assertArrayHasKey('uuid', $after);
$this->assertArrayNotHasKey('id', $after);
}

public function testGetCastsCacheIsInvalidatedBySetKeyType()
{
$model = new ModelStub;

$before = $model->getCasts();
$this->assertSame('int', $before[$model->getKeyName()]);

$model->setKeyType('string');

$after = $model->getCasts();
$this->assertSame('string', $after[$model->getKeyName()]);
}

public function testGetCastsCacheIsInvalidatedBySetIncrementing()
{
$model = new ModelStub;

$with = $model->getCasts();
$this->assertArrayHasKey('id', $with);

$model->setIncrementing(false);

$without = $model->getCasts();
$this->assertArrayNotHasKey('id', $without);
}

public function testGetCastsForNonIncrementingModelReturnsCastsDirectly()
{
$model = new ModelStub;
$model->setIncrementing(false);
$model->mergeCasts(['foo' => 'int']);

$casts = $model->getCasts();

$this->assertArrayNotHasKey($model->getKeyName(), $casts);
$this->assertArrayHasKey('foo', $casts);
$this->assertSame('int', $casts['foo']);
}

public function testGetCastsIsIsolatedPerInstance()
{
$a = new ModelStub;
$b = new ModelStub;

$b->mergeCasts(['extra' => 'int']);

$this->assertArrayNotHasKey('extra', $a->getCasts());
$this->assertArrayHasKey('extra', $b->getCasts());
}

public function testGetCastsOnNewInstanceIsNotSharedWithSource()
{
$source = new ModelStub;
$source->mergeCasts(['a' => 'int']);
$source->getCasts();

$clone = $source->newInstance();
$clone->mergeCasts(['b' => 'string']);

$this->assertArrayHasKey('b', $clone->getCasts());
$this->assertArrayNotHasKey('b', $source->getCasts());
}

public function testGetCastsCacheIsInvalidatedDuringInitializeHasAttributes()
{
Model::clearBootedModels();

Model::setEventDispatcher($dispatcher = new EventDispatcher);

try {
$dispatcher->listen(
'eloquent.booting: ' . GetCastsBootingStub::class,
function (GetCastsBootingStub $model) {
$model->getCasts();
}
);

$instance = new GetCastsBootingStub;

$casts = $instance->getCasts();

$this->assertArrayHasKey('foo', $casts);
$this->assertSame('integer', $casts['foo']);
} finally {
GetCastsBootingStub::flushEventListeners();
Model::clearBootedModels();
Model::unsetEventDispatcher();
}
}

public function testGetCastsCacheIsInvalidatedDuringInitializeSoftDeletes()
{
Model::clearBootedModels();

Model::setEventDispatcher($dispatcher = new EventDispatcher);

try {
$dispatcher->listen(
'eloquent.booting: ' . GetCastsSoftDeletingBootingStub::class,
function (GetCastsSoftDeletingBootingStub $model) {
$model->getCasts();
}
);

$instance = new GetCastsSoftDeletingBootingStub;

$casts = $instance->getCasts();

$this->assertArrayHasKey('deleted_at', $casts);
$this->assertSame('datetime', $casts['deleted_at']);
} finally {
GetCastsSoftDeletingBootingStub::flushEventListeners();
Model::clearBootedModels();
Model::unsetEventDispatcher();
}
}

public function testMergeCastsMergesCastsUsingArrays()
{
$model = new CastingStub;
Expand Down Expand Up @@ -4470,6 +4617,23 @@ public function __toString()
}
}

class GetCastsBootingStub extends Model
{
protected array $guarded = [];

protected function casts(): array
{
return ['foo' => 'integer'];
}
}

class GetCastsSoftDeletingBootingStub extends Model
{
use SoftDeletes;

protected array $guarded = [];
}

enum ConnectionName
{
case Foo;
Expand Down
8 changes: 4 additions & 4 deletions tests/Database/DatabaseEloquentPolymorphicIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public function testItLoadsRelationshipsAutomatically()
$like = LikeWithSingleWith::first();

$this->assertTrue($like->relationLoaded('likeable'));
$this->assertEquals(Comment::first(), $like->likeable);
$this->assertTrue($like->likeable->is(Comment::first()));
}

public function testItLoadsChainedRelationshipsAutomatically()
Expand All @@ -93,7 +93,7 @@ public function testItLoadsChainedRelationshipsAutomatically()
$like = LikeWithSingleWith::first();

$this->assertTrue($like->likeable->relationLoaded('commentable'));
$this->assertEquals(Post::first(), $like->likeable->commentable);
$this->assertTrue($like->likeable->commentable->is(Post::first()));
}

public function testItLoadsNestedRelationshipsAutomatically()
Expand All @@ -105,7 +105,7 @@ public function testItLoadsNestedRelationshipsAutomatically()
$this->assertTrue($like->relationLoaded('likeable'));
$this->assertTrue($like->likeable->relationLoaded('owner'));

$this->assertEquals(User::first(), $like->likeable->owner);
$this->assertTrue($like->likeable->owner->is(User::first()));
}

public function testItLoadsNestedRelationshipsOnDemand()
Expand All @@ -117,7 +117,7 @@ public function testItLoadsNestedRelationshipsOnDemand()
$this->assertTrue($like->relationLoaded('likeable'));
$this->assertTrue($like->likeable->relationLoaded('owner'));

$this->assertEquals(User::first(), $like->likeable->owner);
$this->assertTrue($like->likeable->owner->is(User::first()));
}

public function testItLoadsNestedMorphRelationshipsOnDemand()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ public function testUpdateModelAfterSoftDeleting()
$userModel->delete();
$this->assertEquals($now->toDateTimeString(), $userModel->getOriginal('deleted_at'));
$this->assertNull(User::find(2));
$this->assertEquals($userModel, User::withTrashed()->find(2));
$this->assertTrue($userModel->is(User::withTrashed()->find(2)));
}

/**
Expand Down
13 changes: 13 additions & 0 deletions tests/Database/DatabaseEloquentWithCastsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Hypervel\Tests\Database;

use Carbon\CarbonInterface;
use Hypervel\Database\Capsule\Manager as DB;
use Hypervel\Database\Eloquent\MissingAttributeException;
use Hypervel\Database\Eloquent\Model;
Expand Down Expand Up @@ -80,6 +81,18 @@ public function testWithCreateOrFirst()
$this->assertSame($time1->id, $time2->id);
}

public function testWithCastsDoesNotLeakAcrossQueries()
{
Time::query()->insert(['time' => '07:30']);

$scoped = Time::query()->withCasts(['time' => 'string'])->first();
$this->assertIsString($scoped->time);
$this->assertSame('07:30', $scoped->time);

$default = Time::query()->first();
$this->assertInstanceOf(CarbonInterface::class, $default->time);
}

public function testThrowsExceptionIfCastableAttributeWasNotRetrievedAndPreventMissingAttributesIsEnabled()
{
Time::create(['time' => now()]);
Expand Down