orchidsoftware / platform

Orchid is a @laravel package that allows for rapid application development of back-office applications, admin/user panels, and dashboards.
https://orchid.software
MIT License
4.27k stars 631 forks source link

DateTimer with custom format throws an exception #2544

Open plamenradev opened 1 year ago

plamenradev commented 1 year ago

Describe the bug DateTimer with custom format throws exception if any other field is filled incorrect.

To Reproduce

  1. Create form layout with DateTimer (lets say for date of birth - DOB) and at least one more other type field, for example simple Input for email.
  2. Set format and serverFormat on the DateTimer, for example "d/m/Y"
  3. Create validation rules - for DateTimer: "date_format:d/m/Y", for the other Input field - for example "unique:users,email"
  4. Fill the form with any date in the DateTimer and email, which is already taken by another user.
  5. After submit it throws an InvalidFormatException: Could not parse '22/03/2023': Failed to parse time string (22/03/2023) at position 0 (2): Unexpected character
  6. If the email validation is OK - there is no InvalidFormatException and everything is working fine.

Layout:

class UserEditLayout extends Rows
{
    public function fields(): array
    {
        return [
            Input::make('user.email')
                ->type('email')
                ->title(__('Email Address')),

            DateTimer::make('user.dob')
                ->title('Date of Birth')
                ->allowInput()
                ->serverFormat(config('app.date_format')) // Set to d/m/Y
                ->format(config('app.date_format')) // Set to d/m/Y
                ->help(__('Format DD/MM/YYYY')),
        ];
    }
}

Screen save method:

public function save(User $user, UserRequest $request)
{
    $request->validate($request->rules());
    $user->when($request->filled('user.password'), function (Builder $builder) use ($request) {
        $builder->getModel()->password = Hash::make($request->input('user.password'));
    });

    $user
        ->fill(collect($request->validated('user'))->except('password')->all())
        ->save();

    Toast::info(__('User saved'));
}

UserRequest class:

class UserRequest extends FormRequest
{
    public function rules()
    {
        $ignoreId = $this->route('user') ? ',' . $this->route('user')->id : '';

        return [
            'user.email' => 'email|nullable|unique:users,email' . $ignoreId,
            'user.dob' => 'date_format:' . config('app.date_format') . '|nullable', // Set to d/m/Y
        ];
    }

    public function validated($key = null, $default = null)
    {
        $validated = data_get($this->validator->validated(), $key, $default);
        $merged = collect($validated);

        if ($merged->get('dob')) {
            $merged->put('dob', Carbon::createFromFormat(config('app.date_format'), $merged->get('dob'))->format('Y-m-d'));
        }

        return $merged->all();
    }
}

Expected behavior Correct parsing the date

Server (please complete the following information):

Additional context The date_of_birth fields in the database has type date I tried to play with prepareForValidation() and failedValidation() methods in the UserRequest class, but it still throws an exception.

plamenradev commented 1 year ago

When you use a custom format for DateTimer and it throws an validation exception - you need to return back the date value in such a format, that could be parsed by Carbon::parse(), so here is an ugly solution, but it works, just for letting you know the idea.

In UserRequest class overwrite the following function:

protected function failedValidation(Validator $validator): void
{
}

In Screen save method:

public function save(User $user, UserRequest $request)
{
    $validator = Validator::make($request->input(), $request->rules());

    if ($validator->fails()) {
        $input = $request->input();
        data_set($input, 'user.dob', Carbon::createFromFormat(config('app.date_format'), $request->input('user.dob'))->format('Y-m-d'));

        return redirect(url()->previous())
            ->withInput($input)
            ->withErrors($validator->errors()->messages());
    }

    ...
}