Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenvanassche committed Jul 19, 2023
1 parent 83c46bf commit 575c40d
Show file tree
Hide file tree
Showing 16 changed files with 467 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/Casts/Cast.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
interface Cast
{
public function cast(DataProperty $property, mixed $value, array $context): mixed;

public function preValidationRules(DataProperty $property, mixed $value): array;
}
5 changes: 5 additions & 0 deletions src/Casts/DateTimeInterfaceCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ public function cast(DataProperty $property, mixed $value, array $context): Date

return $datetime;
}

public function preValidationRules(DataProperty $property, mixed $value): array
{

}
}
5 changes: 5 additions & 0 deletions src/Casts/EnumCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public function cast(DataProperty $property, mixed $value, array $context): Back
throw CannotCastEnum::create($type, $value);
}
}

public function preValidationRules(DataProperty $property, mixed $value): array
{

}
}
4 changes: 3 additions & 1 deletion src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe;
use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\InitialValidationDataPipe;
use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe;
use Spatie\LaravelData\PaginatedDataCollection;
Expand Down Expand Up @@ -86,8 +87,9 @@ public static function pipeline(): DataPipeline
->through(AuthorizedDataPipe::class)
->through(MapPropertiesDataPipe::class)
->through(FillRouteParameterPropertiesDataPipe::class)
->through(ValidatePropertiesDataPipe::class)
->through(DefaultValuesDataPipe::class)
->through(ValidatePropertiesDataPipe::class)
// ->through(InitialValidationDataPipe::class)
->through(CastPropertiesDataPipe::class);
}

Expand Down
1 change: 0 additions & 1 deletion src/DataPipes/FillRouteParameterPropertiesDataPipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,5 @@ protected function resolveValue(
}

return data_get($parameter, $attribute->property ?? $dataProperty->name);
;
}
}
24 changes: 24 additions & 0 deletions src/DataPipes/InitialValidationDataPipe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Spatie\LaravelData\DataPipes;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Spatie\LaravelData\Rules\InitialPropertyRule;
use Spatie\LaravelData\Support\DataClass;

class InitialValidationDataPipe implements DataPipe
{
public function handle(mixed $payload, DataClass $class, Collection $properties): Collection
{
$rules = [];

foreach ($class->properties as $property) {
$rules[$property->name] = new InitialPropertyRule($property);
}

Validator::make($properties->toArray(), $rules)->validate();

return $properties;
}
}
157 changes: 157 additions & 0 deletions src/Rules/InitialPropertyRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

namespace Spatie\LaravelData\Rules;

use Closure;
use Exception;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Validator;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\Types\MultiType;
use Spatie\LaravelData\Support\Types\PartialType;
use Spatie\LaravelData\Support\Types\SingleType;
use Spatie\LaravelData\Support\Types\UndefinedType;
use Stringable;

class InitialPropertyRule implements ValidationRule
{
public function __construct(
protected DataProperty $property
) {
}

public function validate(string $attribute, mixed $value, Closure $fail): void
{
$dataType = $this->property->type;
$type = $dataType->type;

if ($dataType->isMixed() || $type instanceof UndefinedType) {
return;
}

if (! $dataType->isNullable() && $value === null) {
$fail(__('validation.required', ['attribute' => $attribute]));
}

if ($dataType->isNullable() && $value === null) {
return;
}

if ($dataType->type instanceof SingleType) {
$this->validateSingleType($dataType->type, $attribute, $value, $fail);

return;
}

if ($dataType->type instanceof MultiType) {
$this->validateMultiType($dataType->type, $attribute, $value, $fail);

return;
}

throw new Exception('Unexpected path, we should never get here');
}

protected function validateSingleType(
SingleType $singleType,
string $attribute,
mixed $value,
Closure $fail,
): void {
$validity = $this->isPartialTypeValid($singleType->type, $attribute, $value);

if ($validity === true) {
return;
}

$fail($validity);
}

protected function validateMultiType(
MultiType $multiType,
string $attribute,
mixed $value,
Closure $fail,
): void {
$invalidMessages = [];

foreach ($multiType->types as $type) {
$validity = $this->isPartialTypeValid($type, $attribute, $value);

if ($validity === true) {
return;
}

$invalidMessages[] = $validity;
}

$fail(implode(' or ', $invalidMessages));
}

protected function isPartialTypeValid(

Check failure on line 91 in src/Rules/InitialPropertyRule.php

View workflow job for this annotation

GitHub Actions / P8.1 - L9.* - prefer-lowest - ubuntu-latest

Cannot use 'Spatie\LaravelData\Rules\true' as class name as it is reserved

Check failure on line 91 in src/Rules/InitialPropertyRule.php

View workflow job for this annotation

GitHub Actions / P8.1 - L9.* - prefer-stable - ubuntu-latest

Cannot use 'Spatie\LaravelData\Rules\true' as class name as it is reserved

Check failure on line 91 in src/Rules/InitialPropertyRule.php

View workflow job for this annotation

GitHub Actions / P8.1 - L10.* - prefer-stable - ubuntu-latest

Cannot use 'Spatie\LaravelData\Rules\true' as class name as it is reserved
PartialType $type,
string $attribute,
mixed $value
): true|string {
// TODO: casts always have precedence over here
if ($this->property->cast) {
return Validator::make(
[$attribute => $value],
[$attribute => $this->property->cast->preValidationRules($this->property, $value)]
)->validate();
}

if ($type->name === 'string' && ! $this->isValidString($value)) {
return __('validation.string', ['attribute' => $attribute]);
}

if ($type->name === 'bool' && ! $this->isValidBool($value)) {
return __('validation.boolean', ['attribute' => $attribute]);
}

if ($type->name === 'int' && ! $this->isValidIntOrFloat($value)) {
return __('validation.integer', ['attribute' => $attribute]);
}

if ($type->name === 'float' && ! $this->isValidIntOrFloat($value)) {
return __('validation.float', ['attribute' => $attribute]);
}

if ($type->name === 'array' && ! $this->isValidArray($value)) {
return __('validation.array', ['attribute' => $attribute]);
}

// We're having an object over here, this should basically be catched by the casts
// Unless it is a data object
// Data objects can be created using magic methods, so we need check these types and count them all up
// Data objects also can be created using from which accepts all values based upon their normalizer
// Then there are datacollectables, these we can also automatically create
// Again magic collect methods which can take any type
// + some default types
// For each item in such a collection we need to, again check the data objects

return true;
}

protected function isValidString(mixed $value): bool
{
return is_scalar($value) || $value instanceof Stringable;
}

protected function isValidBool(mixed $value): bool
{
return is_scalar($value);
}

protected function isValidIntOrFloat(mixed $value): bool
{
return is_numeric($value) || is_bool($value);
}

protected function isValidArray(mixed $value): bool
{
return is_array($value); // TODO what if we have an arrayble? A) cast before validation, B) split casting and decide a cast before validation
// Follow up -> add a new pipe, auto infer casts, which automatically adds global casts
// We'll add support for a collection global cast from array which solves our problem
}
}
18 changes: 18 additions & 0 deletions src/Support/TransientProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Spatie\LaravelData\Support;

use Spatie\LaravelData\Casts\Cast;

class TransientProperty
{
public function __construct(
public mixed $value,
public DataProperty $property,
public ?Cast $cast = null, // Pre determined cast -> see idea to set this in stone on the data property
public bool $isExactValue = false, // Whether an object is exaxctly what the property needed,
public bool $noCastFound = false, // Whether no cast was found for the property
public ?DataMethod $magicMethod = null, // A determined magic method that can be used to create the value
) {
}
}
47 changes: 47 additions & 0 deletions tests/DataPipes/InitialValidationDataPipeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

use Spatie\LaravelData\Data;

it('can handle multiple cases', function () {
$stringable = new class implements Stringable {
public function __toString()
{
return 'stringable';
}
};

class DataTestBreakableValidation extends Data
{
// strict types are not a problem since this all happens in the data context which is not strict
public function __construct(
// Takes everything
public mixed $mixed,
// Takes string, bool, float, int, class implementing Stringable
public string $string,
// Takes string, bool, float, int
public bool $bool,
// Takes bool, float, int
public int $int,
// Takes bool, float, int
public float $float,
// Takes array
public array $array,
// Takes string, bool, float, int, class implementing Stringable, null
public ?string $nullable,
) {
}
}


$data = new DataTestBreakableValidation(
'mixed',
'string',
'1d',
'12',
3.14,
[],
null,
);

expect($data)->toBeInstanceOf(DataTestBreakableValidation::class);
});
5 changes: 5 additions & 0 deletions tests/Fakes/Castables/SimpleCastable.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public function cast(DataProperty $property, mixed $value, array $context): mixe
{
return new SimpleCastable($value);
}

public function preValidationRules(DataProperty $property, mixed $value): array
{

}
};
}
}
5 changes: 5 additions & 0 deletions tests/Fakes/Casts/ConfidentialDataCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ public function cast(DataProperty $property, mixed $value, array $context): Simp
{
return SimpleData::from('CONFIDENTIAL');
}

public function preValidationRules(DataProperty $property, mixed $value): array
{

}
}
5 changes: 5 additions & 0 deletions tests/Fakes/Casts/ConfidentialDataCollectionCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ public function cast(DataProperty $property, mixed $value, array $context): arra
{
return array_map(fn () => SimpleData::from('CONFIDENTIAL'), $value);
}

public function preValidationRules(DataProperty $property, mixed $value): array
{

}
}
5 changes: 5 additions & 0 deletions tests/Fakes/Casts/ContextAwareCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ public function cast(DataProperty $property, mixed $value, array $context): mixe
{
return $value . '+' . json_encode($context);
}

public function preValidationRules(DataProperty $property, mixed $value): array
{

}
}
5 changes: 5 additions & 0 deletions tests/Fakes/Casts/StringToUpperCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ public function cast(DataProperty $property, mixed $value, array $context): stri
{
return strtoupper($value);
}

public function preValidationRules(DataProperty $property, mixed $value): array
{

}
}
Loading

0 comments on commit 575c40d

Please sign in to comment.