Date formats altered on form after validation error

Hello,

I’m using Yii2 for the first time. It’s a powerful tool, but it’s not always easy to understand the sequence of interactions.

At the moment I’m stuck on an irritating problem concerning the date format :

As a French speaker, I use dates in the dd/MM/yyyy format. So I’ve defined a formatter in my web.php configuration file:

        'formatter' => [
            'dateFormat' => 'dd/MM/yyyy',
            'decimalSeparator' => ',',
            'thousandSeparator' => '.',
            'currencyCode' => 'EUR',
        ],

And I redefine the beforeSave function in my models to convert to MySql format. For example :

    public function beforeSave($insert) {
        if (!parent::beforeSave($insert)) {
           return false;
        }
        $this->Date_of_birth = SetDate::convert(strtotime(str_replace("/","-",$this->Date_of_birth)));                
        return true;
    }    

With : SetDate component

namespace app\components;

class SetDate {
    const DATE_FORMAT = 'php:Y-m-d';
    const DATETIME_FORMAT = 'php:Y-m-d H:i:s';
    const TIME_FORMAT = 'php:H:i:s';

    public static function convert($dateStr, $type='date', $format = null) {
        if ($type === 'datetime') {
              $fmt = ($format == null) ? self::DATETIME_FORMAT : $format;
        }
        elseif ($type === 'time') {
              $fmt = ($format == null) ? self::TIME_FORMAT : $format;
        }
        else {
              $fmt = ($format == null) ? self::DATE_FORMAT : $format;
        }
        return \Yii::$app->formatter->asDate($dateStr, $fmt);
    }
}

I use DatePicker in my forms :

    <?= $form->field($model, 'Date_of_birth')->widget(\yii\jui\DatePicker::classname(),[]) ?>   

It all works fine … except when I get validation errors in my form. As soon as I get a validation error (on any field), the form is redisplayed with the error messages … but all the date fields are redisplayed in MM/dd/yyyy format !

I can’t figure out what the problem is, and I have no idea how to solve it. Can anyone help me?

Many thanks in advance.

André

Uh, so like, if you wanna make sure your date shows up all right after some stupid validation errors, here’s what you gotta do, okay?

First thing, you gotta set your application language and formatter locale to French, like for real. So in your config, you do:

'formatter' => [
	'locale' => 'fr-FR',// Set locale explicitly
	'dateFormat' => 'dd/MM/yyyy',// your preferred format
	'decimalSeparator' => ',',
	'thousandSeparator' => '.',
	'currencyCode' => 'EUR',
],

And also, don’t forget to set the app language at the top level:

'language' => 'fr-FR',

This way Yii understands, okay, I gotta be all French style and stuff.

Now, uh, the \yii\jui\DatePicker thingy, it uses jQuery UI DatePicker, but by default it’s like all American-ish (mm/dd/yyyy), so you gotta tell it, “Hey, be French too!”

Like this:

<?= $form->field($model, 'Date_of_birth')->widget(\yii\jui\DatePicker::classname(), [
    'language' => 'fr',
    'dateFormat' => 'dd/MM/yyyy',
]) ?>

Alright, so then, here’s the tricky part—your model gets the date in dd/MM/yyyy, but MySQL wants it as YYYY-MM-DD. So, before you validate or save, you gotta transform it, or else… uh, bad things happen.

You do this by overriding beforeValidate():

public function beforeValidate()
{
	if (parent::beforeValidate())
	{
		if (!empty($this->Date_of_birth))
		{
			// Convert date from dd/MM/yyyy to Y-m-d
			$date = \DateTime::createFromFormat('d/m/Y', $this->Date_of_birth);
			
			if ($date !== false)
			{
				$this->Date_of_birth = $date->format('Y-m-d');
			}
			else
			{
				// Invalid date format, leave as is to trigger validation error
			}
		}
		
		return true;
	}
	
	return false;
}

And after you get the data from the database, you wanna change the date back to dd/MM/yyyy to show in the form. So in afterFind():

public function afterFind()
{
	parent::afterFind();
	
	if (!empty($this->Date_of_birth))
	{
		$date = \DateTime::createFromFormat('Y-m-d', $this->Date_of_birth);
		
		if ($date !== false)
		{
			$this->Date_of_birth = $date->format('d/m/Y'); // for form display
		}
	}
}

But if you wanna be super organized, you can make a helper class to handle the dates so you don’t screw up everywhere, y’know? Like this: Helpers: Helpers Overview | The Definitive Guide to Yii 2.0 | Yii PHP Framework

declare(strict_types=1);

namespace app\helpers;

use DateTime;
use DateTimeImmutable;

use Yii;

class DateHelper
{
	// For Yii formatter & JS dateFormat
	const DISPLAY_FORMAT = 'dd/MM/yyyy';
	
	private const PHP_DISPLAY_FORMAT = 'd/m/Y';
	private const PHP_STORAGE_FORMAT = 'Y-m-d';
	
	// Convert date from display format (dd/MM/yyyy) to storage format (Y-m-d)
	public static function toStorageFormat(string $dateStr): ?string
	{
		$date = DateTime::createFromFormat(self::PHP_DISPLAY_FORMAT, $dateStr);
		$errors = DateTime::getLastErrors();
		
		if ($date === false || $errors['warning_count'] > 0 || $errors['error_count'] > 0)
		{
			return null;
		}
		
		return $date->format(self::PHP_STORAGE_FORMAT);
	}
	
	// Convert date from storage format (Y-m-d) to display format (dd/MM/yyyy) using Yii formatter
	public static function toDisplayFormat($dateStr)
	{
		if ($dateStr === '')
		{
			return null;
		}
		
		// Validate strict format using DateTimeImmutable for immutability
		$date = DateTimeImmutable::createFromFormat(self::PHP_STORAGE_FORMAT, $dateStr);
		$errors = DateTimeImmutable::getLastErrors();
		
		if ($date === false || $errors['warning_count'] > 0 || $errors['error_count'] > 0)
		{
			return null;
		}
		
		// Use Yii formatter to format date with locale-aware output
		return \Yii::$app->formatter->asDate($date->getTimestamp(), self::DISPLAY_FORMAT);
	}
	
	// Format any given date value using Yii formatter.
	public static function formatDate(string|int $value, string $format = self::DISPLAY_FORMAT): ?string
	{
		if ($value === '' || $value === 0)
		{
			return null;
		}
		
		$timestamp = is_int($value) ? $value : strtotime($value);
		
		if ($timestamp === false || $timestamp < 0)
		{
			return null;
		}
		
		return \Yii::$app->formatter->asDate($timestamp, $format);
	}
}

And then in your model, instead of writing the date conversion yourself all over, just use the
helper like this:

use app\helpers\DateHelper;

public function beforeValidate()
{
	if (parent::beforeValidate())
	{
		if (!empty($this->Date_of_birth))
		{
			$converted = DateHelper::toStorageFormat($this->Date_of_birth);
			
			if (!$converted)
			{
				$this->addError('Date_of_birth', 'Date invalide (jj/mm/aaaa)');
				
				return false;
			}
			
			$this->Date_of_birth = $converted;
		}
		
		return true;
	}
	
	return false;
}

public function afterFind()
{
	parent::afterFind();
	
	if (!empty($this->Date_of_birth))
	{
		$displayDate = DateHelper::toDisplayFormat($this->Date_of_birth);
		
		if ($displayDate !== null)
		{
			$this->Date_of_birth = $displayDate;
		}
	}
}

Oh! And don’t forget to set your formatter in config/web.php like this:

'formatter' => [
	'class' => 'yii\i18n\Formatter',
	'locale' => 'fr-FR',
	'dateFormat' => 'php:d/m/Y',// can use PHP format string here
	'datetimeFormat' => 'php:d/m/Y H:i:s',
	'timeFormat' => 'php:H:i:s',
	'decimalSeparator' => ',',
	'thousandSeparator' => '.',
	'currencyCode' => 'EUR',
],

So umm, yeah, that’s basically it. Just gotta tell the system to speak French everywhere, convert your dates right before saving, then convert them back for display, and maybe use the helper so you don’t mess up.

Hope this helps! I-I’m not Rick, so if this sounds a little clumsy… well, at least I tried, okay? Geez.

2 Likes

Hi Ricksmort,

First of all, thanks a lot for your quick and very exhaustive reply, which enabled me to solve my problem, with a few adjustments:

  1. I had already set the language to fr globally, but not in the formatter. So I did it.
  2. I adapted my DatePickers.
  3. Date conversion in afterFind is not necessary. Apparently the formatter takes care of this and automatically converts them to the fr format.
  4. Then I’ve tried overriding beforeValidate, but it doesn’t work. I get “Invalid date format” on every date.
  5. So I’ve thought about my problem: in fact, I don’t have a validation problem, as I said it’s when the dates are redisplayed after any validation error that the date formats are changed. So I tried changing your beforeValidate to afterValidate overriding. And it now works perfectly.

This seems consistent to me. If the formatter is capable of automatically converting the dates supplied by the database from “Y-m-d” format to “d/m/Y” format, by returning the dates to “Y-m-d” format after validation, it converts them back to “d/m/Y” before displaying them again.

Once again thank you.

André

1 Like

Oh geez, hey, uh, thanks for the detailed update!

I mean, that sounds super complicated, but it’s great you got it working by switching from beforeValidate to afterValidate — that kinda makes sense, y’know? Like, the dates gotta be in the right format after everything’s checked, not before. And the formatter doing the heavy lifting on the conversion? Man, that’s pretty slick!

I’m just glad my input helped you figure it out without blowing up the whole thing! If you run into any more weird date stuff or, uh, other confusing stuff, just lemme know. Science and code are tricky, but we’ll get through it.

Wubba lubba dub dub!

1 Like

Good to know I’ve somebody to ask. Hope it’ll not be necessary, but who knows ?
:grinning: :+1: