Skip to content

Commit

Permalink
Merge branch 'main' into feat/ability_to_encrypt_eloquent_cast
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenvanassche authored Jun 13, 2024
2 parents de5d11b + 691f909 commit 09dc253
Show file tree
Hide file tree
Showing 17 changed files with 202 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ jobs:
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/pest
run: vendor/bin/pest --enforce-time-limit --fail-on-risky
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"require-dev" : {
"fakerphp/faker": "^1.14",
"friendsofphp/php-cs-fixer": "^3.0",
"inertiajs/inertia-laravel": "dev-master#4508fd1",
"inertiajs/inertia-laravel": "^1.2",
"livewire/livewire": "^3.0",
"mockery/mockery": "^1.6",
"nesbot/carbon": "^2.63",
Expand Down
7 changes: 3 additions & 4 deletions src/Attributes/Validation/GreaterThan.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class GreaterThan extends StringValidationAttribute
{
protected FieldReference $field;
protected FieldReference|int|float $field;

public function __construct(
string|FieldReference $field,
int|float|string|FieldReference $field,
) {
$this->field = $this->parseFieldReference($field);
$this->field = is_numeric($field) ? $field : $this->parseFieldReference($field);
}


public static function keyword(): string
{
return 'gt';
Expand Down
6 changes: 3 additions & 3 deletions src/Attributes/Validation/GreaterThanOrEqualTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class GreaterThanOrEqualTo extends StringValidationAttribute
{
protected FieldReference $field;
protected int|float|FieldReference $field;

public function __construct(
string|FieldReference $field,
int|float|string|FieldReference $field,
) {
$this->field = $this->parseFieldReference($field);
$this->field = is_numeric($field) ? $field : $this->parseFieldReference($field);
}


Expand Down
6 changes: 3 additions & 3 deletions src/Attributes/Validation/LessThan.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class LessThan extends StringValidationAttribute
{
protected FieldReference $field;
protected int|float|FieldReference $field;

public function __construct(
string|FieldReference $field,
int|float|string|FieldReference $field,
) {
$this->field = $this->parseFieldReference($field);
$this->field = is_numeric($field) ? $field : $this->parseFieldReference($field);
}


Expand Down
6 changes: 3 additions & 3 deletions src/Attributes/Validation/LessThanOrEqualTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class LessThanOrEqualTo extends StringValidationAttribute
{
protected FieldReference $field;
protected int|float|FieldReference $field;

public function __construct(
string|FieldReference $field,
int|float|string|FieldReference $field,
) {
$this->field = $this->parseFieldReference($field);
$this->field = is_numeric($field) ? $field : $this->parseFieldReference($field);
}


Expand Down
38 changes: 38 additions & 0 deletions src/Casts/BuiltinTypeCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Spatie\LaravelData\Casts;

use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty;

class BuiltinTypeCast implements Cast, IterableItemCast
{
/**
* @param 'bool'|'int'|'float'|'array'|'string' $type
*/
public function __construct(
protected string $type,
) {
}

public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed
{
return $this->runCast($value);
}

public function castIterableItem(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed
{
return $this->runCast($value);
}

protected function runCast(mixed $value): mixed
{
return match ($this->type) {
'bool' => (bool) $value,
'int' => (int) $value,
'float' => (float) $value,
'array' => (array) $value,
'string' => (string) $value,
};
}
}
5 changes: 5 additions & 0 deletions src/DataPipes/CastPropertiesDataPipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Spatie\LaravelData\DataPipes;

use Illuminate\Support\Enumerable;
use Spatie\LaravelData\Casts\BuiltinTypeCast;
use Spatie\LaravelData\Casts\IterableItemCast;
use Spatie\LaravelData\Casts\Uncastable;
use Spatie\LaravelData\Enums\DataTypeKind;
Expand Down Expand Up @@ -207,6 +208,10 @@ protected function findCastForIterableItems(
}
}

if(in_array($property->type->iterableItemType, ['bool', 'int', 'float', 'array', 'string'])) {
return new BuiltinTypeCast($property->type->iterableItemType);
}

return null;
}
}
11 changes: 9 additions & 2 deletions src/DataPipes/FillRouteParameterPropertiesDataPipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public function handle(
continue;
}

if (! $attribute->replaceWhenPresentInBody && array_key_exists($dataProperty->name, $properties)) {
// if inputMappedName exists, use it first
$name = $dataProperty->inputMappedName ?: $dataProperty->name;
if (! $attribute->replaceWhenPresentInBody && array_key_exists($name, $properties)) {
continue;
}

Expand All @@ -41,7 +43,12 @@ public function handle(
continue;
}

$properties[$dataProperty->name] = $this->resolveValue($dataProperty, $attribute, $parameter);
$properties[$name] = $this->resolveValue($dataProperty, $attribute, $parameter);

// keep the original property name
if ($name !== $dataProperty->name) {
$properties[$dataProperty->name] = $properties[$name];
}
}

return $properties;
Expand Down
43 changes: 24 additions & 19 deletions src/Normalizers/Normalized/NormalizedModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,20 @@ public function getProperty(string $name, DataProperty $dataProperty): mixed
{
$propertyName = $this->model::$snakeAttributes ? Str::snake($name) : $name;

return $this->properties[$propertyName] ?? $this->fetchNewProperty($propertyName, $dataProperty);
$value = array_key_exists($propertyName, $this->properties)
? $this->properties[$propertyName]
: $this->fetchNewProperty($propertyName, $dataProperty);

if ($value === null && ! $dataProperty->type->isNullable) {
return UnknownProperty::create();
}

return $value;
}

protected function initialize(Model $model): void
{
$this->properties = $model->toArray();
$this->properties = $model->withoutRelations()->toArray();

foreach ($model->getDates() as $key) {
if (isset($this->properties[$key])) {
Expand All @@ -41,14 +49,6 @@ protected function initialize(Model $model): void
}
}
}

foreach ($model->getRelations() as $key => $relation) {
$key = $model::$snakeAttributes ? Str::snake($key) : $key;

if (isset($this->properties[$key])) {
$this->properties[$key] = $relation;
}
}
}

protected function isDateCast(string $cast): bool
Expand All @@ -69,18 +69,23 @@ protected function fetchNewProperty(string $name, DataProperty $dataProperty): m
return $this->properties[$name] = $this->model->getAttribute($name);
}

if (! $dataProperty->attributes->contains(fn (object $attribute) => $attribute::class === LoadRelation::class)) {
return UnknownProperty::create();
}

$studlyName = Str::studly($name);
$camelName = Str::camel($name);

if (! method_exists($this->model, $studlyName)) {
return UnknownProperty::create();
if ($dataProperty->attributes->contains(fn (object $attribute) => $attribute::class === LoadRelation::class)) {
if (method_exists($this->model, $name)) {
$this->model->loadMissing($name);
} elseif (method_exists($this->model, $camelName)) {
$this->model->loadMissing($camelName);
}
}

$this->model->load($studlyName);
if ($this->model->relationLoaded($name)) {
return $this->properties[$name] = $this->model->getRelation($name);
}
if ($this->model->relationLoaded($camelName)) {
return $this->properties[$name] = $this->model->getRelation($camelName);
}

return $this->properties[$name] = $this->model->{$studlyName};
return $this->properties[$name] = UnknownProperty::create();
}
}
42 changes: 42 additions & 0 deletions tests/CreationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
'boolean' => true,
'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00),
'nullable_date' => null,
'nullable_optional_date' => null,
]);

$dataClass = new class () extends Data {
Expand All @@ -245,6 +246,10 @@
public Carbon $date;

public ?Carbon $nullable_date;

public Optional|Carbon $optional_date;

public Optional|null|Carbon $nullable_optional_date;
};

$data = $dataClass::from(DummyModel::findOrFail($model->id));
Expand All @@ -253,9 +258,13 @@
->string->toEqual('test')
->boolean->toBeTrue()
->nullable_date->toBeNull()
->optional_date->toBeInstanceOf(Optional::class)
->nullable_optional_date->toBeNull()
->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue();
});



it('can create a data object from a stdClass object', function () {
$object = (object) [
'string' => 'test',
Expand Down Expand Up @@ -1073,6 +1082,39 @@ public function __invoke(SimpleData $data)
->toEqual(['a', 'collection']);
})->skip(fn () => config('data.features.cast_and_transform_iterables') === false);

it('will cast iterables into default types', function () {
$dataClass = new class () extends Data {
/** @var array<int, string> */
public array $strings;

/** @var array<int, bool> */
public array $bools;

/** @var array<int, int> */
public array $ints;

/** @var array<int, float> */
public array $floats;

/** @var array<int, array> */
public array $arrays;
};

$data = $dataClass::from([
'strings' => ['Hello', 42, 3.14, true, '0', 'false'],
'bools' => ['Hello', 42, 3.14, true, ['nested'], '0', 'false'],
'ints' => ['Hello', 42, 3.14, true, ['nested'], '0', 'false'],
'floats' => ['Hello', 42, 3.14, true, ['nested'], '0', 'false'],
'arrays' => ['Hello', 42, 3.14, true, ['nested'], '0', 'false'],
]);

expect($data->strings)->toBe(['Hello', '42', '3.14', '1', '0', 'false']);
expect($data->bools)->toBe([true, true, true, true, true, false, true]);
expect($data->ints)->toBe([0, 42, 3, 1, 1, 0, 0]);
expect($data->floats)->toBe([0.0, 42.0, 3.14, 1.0, 1.0, 0.0, 0.0]);
expect($data->arrays)->toEqual([['Hello'], [42], [3.14], [true], ['nested'], ['0'], ['false']]);
})->skip(fn () => config('data.features.cast_and_transform_iterables') === false);

it('keeps the creation context path up to date', function () {
class TestCreationContextCollectorDataPipe implements DataPipe
{
Expand Down
20 changes: 20 additions & 0 deletions tests/Datasets/RulesDataset.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,21 @@ function fixature(
expected: 'gt:field',
);

yield fixature(
attribute: new GreaterThan(10),
expected: 'gt:10',
);

yield fixature(
attribute: new GreaterThanOrEqualTo('field'),
expected: 'gte:field',
);

yield fixature(
attribute: new GreaterThanOrEqualTo('10'),
expected: 'gte:10',
);

yield fixature(
attribute: new Image(),
expected: 'image',
Expand Down Expand Up @@ -306,11 +316,21 @@ function fixature(
expected: 'lt:field',
);

yield fixature(
attribute: new LessThan(10),
expected: 'lt:10',
);

yield fixature(
attribute: new LessThanOrEqualTo('field'),
expected: 'lte:field',
);

yield fixature(
attribute: new LessThanOrEqualTo(10),
expected: 'lte:10',
);

yield fixature(
attribute: new ListType(),
expected: 'list',
Expand Down
4 changes: 4 additions & 0 deletions tests/Fakes/Models/DummyModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class DummyModel extends Model
protected $casts = [
'date' => 'datetime',
'nullable_date' => 'datetime',
'optional_date' => 'datetime',
'nullable_optional_date' => 'datetime',
'boolean' => 'boolean',
];

Expand All @@ -22,6 +24,8 @@ public static function migrate()
$blueprint->string('string');
$blueprint->dateTime('date');
$blueprint->dateTime('nullable_date')->nullable();
$blueprint->dateTime('optional_date')->nullable();
$blueprint->dateTime('nullable_optional_date')->nullable();
$blueprint->boolean('boolean');

$blueprint->timestamps();
Expand Down
3 changes: 1 addition & 2 deletions tests/Fakes/Models/DummyModelWithEncryptedCasts.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<?php

namespace Fakes\Models;
namespace Spatie\LaravelData\Tests\Fakes\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\LaravelData\Tests\Fakes\SimpleData;
use Spatie\LaravelData\Tests\Fakes\SimpleDataCollection;
use Spatie\LaravelData\Tests\Fakes\SimpleDataWithDefaultValue;

class DummyModelWithEncryptedCasts extends Model
{
Expand Down
5 changes: 5 additions & 0 deletions tests/Fakes/Models/FakeModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ public function fakeNestedModels(): HasMany
return $this->hasMany(FakeNestedModel::class);
}

public function fake_nested_models_snake_cased(): HasMany
{
return $this->hasMany(FakeNestedModel::class);
}

public function accessor(): Attribute
{
return Attribute::get(fn () => "accessor_{$this->string}");
Expand Down
Loading

0 comments on commit 09dc253

Please sign in to comment.