Skip to content
Open
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
11 changes: 7 additions & 4 deletions src/Concerns/ValidateableData.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Validation\Validator;
use Spatie\LaravelData\Resolvers\DataValidationRulesResolver;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataContainer;
use Spatie\LaravelData\Support\Validation\DataRules;
use Spatie\LaravelData\Support\Validation\ValidationContext;
Expand All @@ -21,29 +22,31 @@
*/
trait ValidateableData
{
public static function validate(Arrayable|array $payload): Arrayable|array
public static function validate(Arrayable|array $payload, ?CreationContext $context = null): Arrayable|array
{
$validator = DataContainer::get()->dataValidatorResolver()->execute(
static::class,
$payload,
$context,
);

return DataContainer::get()->validatedPayloadResolver()->execute(
static::class,
$validator,
$context,
);
}

public static function validateAndCreate(Arrayable|array $payload): static
public static function validateAndCreate(Arrayable|array $payload, ?CreationContext $context = null): static
{
static::validate($payload);
static::validate($payload, $context);

return static::factory()
->withoutValidation()
->from($payload);
}

public static function withValidator(Validator $validator): void
public static function withValidator(Validator $validator, ?CreationContext $context = null): void
{
}

Expand Down
7 changes: 4 additions & 3 deletions src/Contracts/ValidateableData.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Validation\Validator;
use Spatie\LaravelData\Support\Creation\CreationContext;

interface ValidateableData
{
public static function validate(Arrayable|array $payload): Arrayable|array;
public static function validate(Arrayable|array $payload, ?CreationContext $context = null): Arrayable|array;

/**
* @return static
*/
public static function validateAndCreate(Arrayable|array $payload): static;
public static function validateAndCreate(Arrayable|array $payload, ?CreationContext $context = null): static;

public static function withValidator(Validator $validator): void;
public static function withValidator(Validator $validator, ?CreationContext $context = null): void;
}
3 changes: 2 additions & 1 deletion src/DataPipes/ValidatePropertiesDataPipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public function handle(
return $properties;
}

($class->name)::validate($properties);
// Should we replace $properties with the returned validated properties?
($class->name)::validate($properties, $creationContext);

$creationContext->validationStrategy = ValidationStrategy::AlreadyRan;

Expand Down
18 changes: 14 additions & 4 deletions src/Resolvers/DataFromArrayResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Spatie\LaravelData\Exceptions\CannotCreateData;
use Spatie\LaravelData\Exceptions\CannotSetComputedValue;
use Spatie\LaravelData\Optional;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataClass;
use Spatie\LaravelData\Support\DataConfig;
use Spatie\LaravelData\Support\DataParameter;
Expand All @@ -26,11 +27,11 @@ public function __construct(protected DataConfig $dataConfig)
*
* @return TData
*/
public function execute(string $class, array $properties): BaseData
public function execute(string $class, array $properties, CreationContext $creationContext): BaseData
{
$dataClass = $this->dataConfig->getDataClass($class);

$data = $this->createData($dataClass, $properties);
$data = $this->createData($dataClass, $properties, $creationContext);

foreach ($dataClass->properties as $property) {
if (
Expand Down Expand Up @@ -72,11 +73,16 @@ public function execute(string $class, array $properties): BaseData
protected function createData(
DataClass $dataClass,
array $properties,
CreationContext $creationContext,
) {
$constructorParameters = $dataClass->constructorMethod?->parameters;

if ($constructorParameters === null) {
return new $dataClass->name();
return match (true) {
method_exists($dataClass->name, 'makeWithContext') => $dataClass->name::makeWithContext($creationContext),
method_exists($dataClass->name, 'make') => $dataClass->name::make(),
default => new $dataClass->name(),
};
}

$parameters = [];
Expand All @@ -94,7 +100,11 @@ protected function createData(
}

try {
return new $dataClass->name(...$parameters);
return match (true) {
method_exists($dataClass->name, 'makeWithContext') => $dataClass->name::makeWithContext($creationContext, ...$parameters),
method_exists($dataClass->name, 'make') => $dataClass->name::make(...$parameters),
default => new $dataClass->name(...$parameters),
};
} catch (ArgumentCountError $error) {
if ($this->isAnyParameterMissing($dataClass, array_keys($parameters))) {
throw CannotCreateData::constructorMissingParameters(
Expand Down
2 changes: 1 addition & 1 deletion src/Resolvers/DataFromSomethingResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function execute(
$normalizedPayloads
);

return $this->dataFromArrayResolver->execute($class, $properties);
return $this->dataFromArrayResolver->execute($class, $properties, $creationContext);
}

protected function replaceDataClassWithMorphedVersion(
Expand Down
4 changes: 3 additions & 1 deletion src/Resolvers/DataValidatorResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Validation\Validator;
use Spatie\LaravelData\Contracts\BaseData;
use Spatie\LaravelData\Contracts\ValidateableData;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\Validation\DataRules;
use Spatie\LaravelData\Support\Validation\ValidationPath;

Expand All @@ -22,6 +23,7 @@ public function __construct(
public function execute(
string $dataClass,
Arrayable|array $payload,
?CreationContext $context = null,
): Validator {
$payload = $payload instanceof Arrayable ? $payload->toArray() : $payload;

Expand Down Expand Up @@ -49,7 +51,7 @@ public function execute(
$validator->stopOnFirstFailure(app()->call([$dataClass, 'stopOnFirstFailure']));
}

$dataClass::withValidator($validator);
$dataClass::withValidator($validator, $context);

return $validator;
}
Expand Down
4 changes: 3 additions & 1 deletion src/Resolvers/ValidatedPayloadResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
use Illuminate\Validation\ValidationException;
use Spatie\LaravelData\Contracts\BaseData;
use Spatie\LaravelData\Contracts\ValidateableData;
use Spatie\LaravelData\Support\Creation\CreationContext;

class ValidatedPayloadResolver
{
/** @param class-string<ValidateableData&BaseData> $dataClass */
public function execute(
string $dataClass,
Validator $validator
Validator $validator,
?CreationContext $context = null,
): array {
try {
$validator->validate();
Expand Down
58 changes: 29 additions & 29 deletions src/Support/Creation/CreationContextFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,30 @@ public static function createFromConfig(
): self {
$config ??= config('data');

return new self(
dataClass: $dataClass,
validationStrategy: ValidationStrategy::from($config['validation_strategy']),
mapPropertyNames: true,
disableMagicalCreation: false,
useOptionalValues: true,
ignoredMagicalMethods: null,
casts: null,
);
return app(static::class, [
'dataClass' => $dataClass,
'validationStrategy' => ValidationStrategy::from($config['validation_strategy']),
'mapPropertyNames' => true,
'disableMagicalCreation' => false,
'useOptionalValues' => true,
'ignoredMagicalMethods' => null,
'casts' => null,
]);
}

public static function createFromCreationContext(
string $dataClass,
CreationContext $creationContext,
): self {
return new self(
dataClass: $dataClass,
validationStrategy: $creationContext->validationStrategy,
mapPropertyNames: $creationContext->mapPropertyNames,
disableMagicalCreation: $creationContext->disableMagicalCreation,
useOptionalValues: $creationContext->useOptionalValues,
ignoredMagicalMethods: $creationContext->ignoredMagicalMethods,
casts: $creationContext->casts,
);
return app(static::class, [
'dataClass' => $dataClass,
'validationStrategy' => $creationContext->validationStrategy,
'mapPropertyNames' => $creationContext->mapPropertyNames,
'disableMagicalCreation' => $creationContext->disableMagicalCreation,
'useOptionalValues' => $creationContext->useOptionalValues,
'ignoredMagicalMethods' => $creationContext->ignoredMagicalMethods,
'casts' => $creationContext->casts,
]);
}

public function validationStrategy(ValidationStrategy $validationStrategy): self
Expand Down Expand Up @@ -186,17 +186,17 @@ public function withCastCollection(

public function get(): CreationContext
{
return new CreationContext(
dataClass: $this->dataClass,
mappedProperties: [],
currentPath: [],
validationStrategy: $this->validationStrategy,
mapPropertyNames: $this->mapPropertyNames,
disableMagicalCreation: $this->disableMagicalCreation,
useOptionalValues: $this->useOptionalValues,
ignoredMagicalMethods: $this->ignoredMagicalMethods,
casts: $this->casts,
);
return app(CreationContext::class, [
'dataClass' => $this->dataClass,
'mappedProperties' => [],
'currentPath' => [],
'validationStrategy' => $this->validationStrategy,
'mapPropertyNames' => $this->mapPropertyNames,
'disableMagicalCreation' => $this->disableMagicalCreation,
'useOptionalValues' => $this->useOptionalValues,
'ignoredMagicalMethods' => $this->ignoredMagicalMethods,
'casts' => $this->casts,
]);
}

/**
Expand Down
35 changes: 35 additions & 0 deletions tests/MagicalCreationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,38 @@ public static function collectCollection(array $items): Collection

expect(fn () => SimpleData::collect($storage))->toThrow(Exception::class, 'Unable to normalize items');
});

it('can create data custom makeWithContext method', function () {
$data = new class ('') extends Data {
public function __construct(public string $string)
{
}

public static function makeWithContext(CreationContext $context, string $string): static
{
return new self($string . ' Wide Web: ' . $context->validationStrategy->name);
}

public static function make(string $string): static
{
return new self($string . ' Wide Web');
}
};

expect($data::from(['string' => 'Hello World']))->toEqual(new $data('Hello World Wide Web: OnlyRequests'));
});

it('can create data custom make method', function () {
$data = new class ('') extends Data {
public function __construct(public string $string)
{
}

public static function make(string $string): static
{
return new self($string . ' Wide Web');
}
};

expect($data::from(['string' => 'Hello World']))->toEqual(new $data('Hello World Wide Web'));
});
53 changes: 48 additions & 5 deletions tests/ValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Spatie\LaravelData\Optional;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\Creation\ValidationStrategy;
use Spatie\LaravelData\Support\Validation\References\AuthenticatedUserReference;
use Spatie\LaravelData\Support\Validation\References\ContainerReference;
Expand Down Expand Up @@ -892,7 +893,7 @@ public static function rules(): array
return [
'collection' => ['array', 'min:1', 'max:2'],
// TODO: should we allow this, how to handle this?
// 'collection.*.string' => ['required', 'string', 'min:100'],
// 'collection.*.string' => ['required', 'string', 'min:100'],
];
}
};
Expand Down Expand Up @@ -2144,7 +2145,7 @@ public static function redirectRoute(FakeInjectable $injectable): string
$dataClass = new class () extends Data {
public string $property;

public static function withValidator(Validator $validator): void
public static function withValidator(Validator $validator, ?CreationContext $context = null): void
{
$validator->setRules([]);
}
Expand Down Expand Up @@ -2624,11 +2625,11 @@ class TestDataWithMergedRuleset extends Data
{
public function __construct(
#[Max(10)]
public string $array_rules,
public string $array_rules,
#[Max(10)]
public string $string_rules,
public string $string_rules,
#[WithoutValidation]
public string $without_validation,
public string $without_validation,
public TestNestedDataWithMergedRules $nested
) {
}
Expand Down Expand Up @@ -2848,3 +2849,45 @@ public function __construct(public string $a, public DummyBackedEnum $enum)
]);
});
});

it('can access the validator if desired', function () {
class DataWithValidator extends Data
{
protected ?Validator $validator = null;

public function __construct(
public string $property,
) {
}

public function validator(): ?Validator
{
return $this->validator;
}

public static function withValidator(Validator $validator, ?CreationContext $context = null): void
{
if ($context) {
$context->validator = $validator;
}
}

public static function makeWithContext(CreationContext $context, ...$properties): static
{
$data = new static(...$context->validator->validated());
$data->validator = $context->validator;

return $data;
}
};

$this->app->bind(CreationContext::class, fn ($app, $params) => new class (...$params) extends CreationContext {
public ?Validator $validator = null;
});

$request = Request::create('hello', 'POST', ['property' => 'foo', 'unvalidated' => 'bar']);
$data = DataWithValidator::from($request);
expect($data->validator())->toBeInstanceOf(Validator::class);
expect($data->property)->toBe('foo');
expect($data)->not()->toHaveProperty('unvalidated');
});