spatie / laravel-data

Powerful data objects for Laravel
https://spatie.be/docs/laravel-data/
MIT License
1.3k stars 211 forks source link

Exception on validating typed fields #760

Closed Ayiannah closed 6 months ago

Ayiannah commented 6 months ago

✏️ Describe the bug When passing a string that is cast to a boolean with a mapinputname, it returns an Exception when validating

↪️ To Reproduce

<?php

use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataPipeline;
use Spatie\LaravelData\DataPipes\AuthorizedDataPipe;
use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe;
use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty;

beforeAll(function () {
    class BooleanCast implements Cast
    {
        public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): bool
        {
            return match ($value) {
                'yes' => true,
                'no' => false,
            };
        }
    }
});

it('should validate boolean with cast', function () {
    class BaseData extends Data
    {
        #[WithCast(BooleanCast::class)]
        public bool $boolean;
    }

    expect(BaseData::validateAndCreate(['boolean' => 'no']))
        ->toBeInstanceOf(BaseData::class);
});

it('should validate boolean with cast and mapinputname', function () {
    class BaseData2 extends Data
    {
        #[WithCast(BooleanCast::class), MapInputName('bool')]
        public bool $boolean;
    }

    expect(BaseData2::validateAndCreate(['bool' => 'no']))
        ->toBeInstanceOf(BaseData2::class);
});

it('should validate boolean with cast, mapinputname and alternate pipeline order', function () {

    class BaseData3 extends Data
    {
        #[WithCast(BooleanCast::class), MapInputName('bool')]
        public bool $boolean;

        public static function pipeline(): DataPipeline
        {
            return DataPipeline::create()
                ->into(static::class)
                ->through(AuthorizedDataPipe::class)
                ->through(MapPropertiesDataPipe::class)
                ->through(FillRouteParameterPropertiesDataPipe::class)
                ->through(CastPropertiesDataPipe::class)
                ->through(ValidatePropertiesDataPipe::class)
                ->through(DefaultValuesDataPipe::class);
        }
    }

    expect(BaseData3::validateAndCreate(['bool' => 'no']))
        ->toBeInstanceOf(BaseData3::class);
});

it('should validate boolean with cast and alternate pipeline order', function () {

    class BaseData4 extends Data
    {
        #[WithCast(BooleanCast::class)]
        public bool $boolean;

        public static function pipeline(): DataPipeline
        {
            return DataPipeline::create()
                ->into(static::class)
                ->through(AuthorizedDataPipe::class)
                ->through(MapPropertiesDataPipe::class)
                ->through(FillRouteParameterPropertiesDataPipe::class)
                ->through(CastPropertiesDataPipe::class)
                ->through(ValidatePropertiesDataPipe::class)
                ->through(DefaultValuesDataPipe::class);
        }
    }

    expect(BaseData4::validateAndCreate(['boolean' => 'no']))
        ->toBeInstanceOf(BaseData4::class);
});

✅ Expected behavior The field is validated after it's cast to a boolean. It also should not try to validate the original property name.

🖥️ Versions

Laravel: 10.10 Laravel Data: 4.5.1 PHP: 8.2

rubenvanassche commented 6 months ago

Validation always runs before casting, that's at the moment how it works in v5 we're going to try rethinking this but at the moment it is not an easy fix.