Skip to content

Commit

Permalink
Merge pull request #56 from WendellAdriel/attribute-cast-workaround
Browse files Browse the repository at this point in the history
Add cast helper methods
  • Loading branch information
WendellAdriel committed Sep 6, 2023
2 parents 5f508bf + c9eee2c commit dd3a00f
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 31 deletions.
89 changes: 69 additions & 20 deletions src/Concerns/CastValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,86 @@

trait CastValues
{
private static ?array $modelCastableProperties = null;

public static function castAndCreate(array $properties): self
{
$model = new static();

$model->castAndFill($properties);
$model->save();

return $model;
}

/**
* @param Collection<PropertyInfo> $properties
* @param array<string,mixed> $properties
*/
private static function castValues(Model $model, Collection $properties): void
public function castAndFill(array $properties): self
{
foreach ($properties as $key => $value) {
$this->{$key} = $this->hasCast($key)
? $this->castAttribute($key, $this->getValueForCast($key, $value))
: $value;
}

return $this;
}

public function castAndSet(string $property, mixed $value): self
{
$this->{$property} = $this->hasCast($property) ? $this->castAttribute($property, $value) : $value;

return $this;
}

public function castAndUpdate(array $properties): self
{
$this->castAndFill($properties);
$this->save();

return $this;
}

private static function castValues(Model $model): void
{
$casts = self::castValuesForCastAttribute($properties);
$casts = array_merge($casts, self::castValuesForLiftAttribute($properties));
$properties = self::getPropertiesWithAttributes($model);
$casts = self::castableProperties($properties);

$model->mergeCasts($casts);
self::$modelCastableProperties = $model->getCasts();
}

/**
* @param Collection<PropertyInfo> $properties
*/
private static function castValuesForCastAttribute(Collection $properties): array
private static function castableProperties(Collection $properties): array
{
$castableProperties = self::getPropertiesForAttributes($properties, [Cast::class]);
$casts = [];
if (is_null(self::$modelCastableProperties)) {
self::buildCastableProperties($properties);
}

return self::$modelCastableProperties;
}

/**
* @param Collection<PropertyInfo> $properties
*/
private static function buildCastableProperties(Collection $properties): void
{
self::$modelCastableProperties = [];

$castableProperties = self::getPropertiesForAttributes($properties, [Cast::class]);
foreach ($castableProperties as $property) {
$castAttribute = $property->attributes->first(fn ($attribute) => $attribute->getName() === Cast::class);
if (blank($castAttribute)) {
continue;
}

$casts[$property->name] = $castAttribute->getArguments()[0];
self::$modelCastableProperties[$property->name] = $castAttribute->getArguments()[0];
}

return $casts;
}

/**
* @param Collection<PropertyInfo> $properties
*/
private static function castValuesForLiftAttribute(Collection $properties): array
{
$castableProperties = self::getPropertiesForAttributes($properties, [Config::class]);
$casts = [];

foreach ($castableProperties as $property) {
$configAttribute = $property->attributes->first(fn ($attribute) => $attribute->getName() === Config::class);
if (blank($configAttribute)) {
Expand All @@ -62,9 +103,17 @@ private static function castValuesForLiftAttribute(Collection $properties): arra
continue;
}

$casts[$property->name] = $configAttribute->cast;
self::$modelCastableProperties[$property->name] = $configAttribute->cast;
}
}

private function getValueForCast(string $property, mixed $value): mixed
{
$castType = self::$modelCastableProperties[$property] ?? null;

return $casts;
return match ($castType) {
'array', 'collection', 'json', 'object' => ! is_string($value) ? json_encode($value) : $value,
default => $value,
};
}
}
5 changes: 3 additions & 2 deletions src/Lift.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static function bootLift(): void
? self::applyCreateValidations($properties)
: self::applyUpdateValidations($properties);

self::castValues($model, $properties);
self::castValues($model);

$publicProperties = self::getModelPublicProperties($model);
$customColumns = self::customColumns();
Expand Down Expand Up @@ -102,6 +102,7 @@ public function syncOriginal(): void
$properties = self::getPropertiesWithAttributes($this);
$this->applyPrimaryKey($properties);
$this->applyAttributesGuard($properties);
self::castValues($this);
}

public function toArray(): array
Expand Down Expand Up @@ -208,7 +209,7 @@ private static function getPropertyForAttribute(Collection $properties, string $

private static function fillProperties(Model $model): void
{
self::castValues($model, self::getPropertiesWithAttributes($model));
self::castValues($model);

foreach ($model->getAttributes() as $key => $value) {
$model->{$key} = $model->hasCast($key) ? $model->castAttribute($key, $value) : $value;
Expand Down
4 changes: 4 additions & 0 deletions tests/Datasets/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ class Product extends Model
#[Cast('immutable_datetime')]
public CarbonImmutable $expires_at;

#[Cast('array')]
public ?array $json_column;

protected $fillable = [
'name',
'price',
'random_number',
'expires_at',
'json_column',
];
}
2 changes: 1 addition & 1 deletion tests/Datasets/ProductConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ class ProductConfig extends Model
#[Config(fillable: true, cast: 'int', hidden: true, rules: ['required', 'integer'])]
public int $random_number;

#[Config(fillable: true, cast: 'immutable_datetime', rules: ['required', 'date_format:Y-m-d H:i:s'])]
#[Config(fillable: true, cast: 'immutable_datetime', rules: ['required', 'date'])]
public CarbonImmutable $expires_at;
}
98 changes: 90 additions & 8 deletions tests/Feature/CastTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,136 @@
use Tests\Datasets\Product;

it('casts values when creating model', function () {
$product = Product::create([
$product = Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
'json_column' => ['foo' => 'bar'],
]);

expect($product->name)->toBe('Product 1')
->and($product->price)->toBe(10.99)
->and($product->random_number)->toBe(123)
->and($product->expires_at)->toBeInstanceOf(Carbon\CarbonImmutable::class)
->and($product->expires_at->format('Y-m-d H:i:s'))->toBe('2023-12-31 23:59:59');
->and($product->expires_at->format('Y-m-d H:i:s'))->toBe('2023-12-31 23:59:59')
->and($product->json_column)->toBe(['foo' => 'bar']);

$this->assertDatabaseHas(Product::class, [
'name' => 'Product 1',
'price' => 10.99,
'random_number' => 123,
'expires_at' => '2023-12-31 23:59:59',
'json_column' => '{"foo":"bar"}',
]);
});

it('casts values when updating model', function () {
$product = Product::create([
it('casts values when updating model with updateAndCast', function () {
$product = Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
]);

$product->update([
$product->castAndUpdate([
'name' => 'Product 2',
'price' => '20.99',
'random_number' => '456',
'expires_at' => '2024-12-31 23:59:59',
'json_column' => ['foo' => 'bar'],
]);

expect($product->name)->toBe('Product 2')
->and($product->price)->toBe(20.99)
->and($product->random_number)->toBe(456)
->and($product->expires_at)->toBeInstanceOf(Carbon\CarbonImmutable::class)
->and($product->expires_at->format('Y-m-d H:i:s'))->toBe('2024-12-31 23:59:59');
->and($product->expires_at->format('Y-m-d H:i:s'))->toBe('2024-12-31 23:59:59')
->and($product->json_column)->toBe(['foo' => 'bar']);

$this->assertDatabaseHas(Product::class, [
'name' => 'Product 2',
'price' => 20.99,
'random_number' => 456,
'expires_at' => '2024-12-31 23:59:59',
'json_column' => '{"foo":"bar"}',
]);
});

it('casts value when updating model with fillAndCast', function () {
$product = Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
]);

$product->castAndFill([
'name' => 'Product 2',
'price' => '20.99',
'random_number' => '456',
'expires_at' => '2024-12-31 23:59:59',
'json_column' => '{"foo":"bar"}',
]);
$product->save();

expect($product->name)->toBe('Product 2')
->and($product->price)->toBe(20.99)
->and($product->random_number)->toBe(456)
->and($product->expires_at)->toBeInstanceOf(Carbon\CarbonImmutable::class)
->and($product->expires_at->format('Y-m-d H:i:s'))->toBe('2024-12-31 23:59:59')
->and($product->json_column)->toBe(['foo' => 'bar']);

$this->assertDatabaseHas(Product::class, [
'name' => 'Product 2',
'price' => 20.99,
'random_number' => 456,
'expires_at' => '2024-12-31 23:59:59',
'json_column' => '{"foo":"bar"}',
]);
});

it('casts value when updating model with setAndCast', function () {
$product = Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
]);

$product->castAndSet('json_column', '{"foo": "bar"}');
$product->save();

expect($product->name)->toBe('Product 1')
->and($product->price)->toBe(10.99)
->and($product->random_number)->toBe(123)
->and($product->expires_at)->toBeInstanceOf(Carbon\CarbonImmutable::class)
->and($product->expires_at->format('Y-m-d H:i:s'))->toBe('2023-12-31 23:59:59')
->and($product->json_column)->toBe(['foo' => 'bar']);

$this->assertDatabaseHas(Product::class, [
'name' => 'Product 1',
'price' => 10.99,
'random_number' => 123,
'expires_at' => '2023-12-31 23:59:59',
'json_column' => '{"foo":"bar"}',
]);
});

it('casts values when retrieving model', function () {
Product::create([
Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
'json_column' => '{"foo": "bar"}',
]);
$product = Product::query()->first();

expect($product->name)->toBe('Product 1')
->and($product->price)->toBe(10.99)
->and($product->random_number)->toBe(123)
->and($product->expires_at)->toBeInstanceOf(Carbon\CarbonImmutable::class)
->and($product->expires_at->format('Y-m-d H:i:s'))->toBe('2023-12-31 23:59:59');
->and($product->expires_at->format('Y-m-d H:i:s'))->toBe('2023-12-31 23:59:59')
->and($product->json_column)->toBe(['foo' => 'bar']);
});
1 change: 1 addition & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ protected function setUp(): void
$table->float('price');
$table->integer('random_number');
$table->integer('another_random_number')->nullable();
$table->json('json_column')->nullable();
$table->timestamp('expires_at');
$table->timestamps();
});
Expand Down

0 comments on commit dd3a00f

Please sign in to comment.