CakePHP date parsing in patchEntity [duplicate] - datetime

I try to save data from a cakephp 3 form. All data are well saved but datetime not. I've got 2 datetime fields. Those fields are filled by jquery-ui widget.
The problem seems to happened when pacthing entity.
$intervention = $this->Interventions->patchEntity($intervention, $this->request->data);
Debug of $this->request->data :
'user_id' => '1',
'description' => 'test',
'starttime' => '2015/11/15 10:00',
'endtime' => '2015/11/15 12:10'
Debug of my object $intervention after pacthEntity :
object(App\Model\Entity\Intervention)
'id' => (int) 3,
'user_id' => (int) 1,
'description' => 'test',
'starttime' => null,
'endtime' => null
...
starttime and endtime become null and I don't understand why.
Is somebody had this pb before ?
I tried (for debuging and understanding) to force fields value afer patching
and datetime fields in mysql are ok.
$intervention->starttime = date('Y-m-d H:i:s', strtotime($this->request->data['starttime']));
$intervention->endtime = date('Y-m-d H:i:s', strtotime($this->request->data['endtime']));
Thanks for help

Date/time values are being casted/parsed in a locale aware fashion
Update: this is the default behavior with the CakePHP application template versions prior to 3.2.5. As of 3.2.5 locale parsing is not enabled by default anymore, which will make the date/time marshalling logic expect a default format of Y-m-d H:i:s instead.
In the marshalling process, values are being "casted" according to the respective column types. For DATETIME columns, this is done by the \Cake\Database\Type\DateTimeType type class.
To be exact, this is done in \Cake\Database\Type\DateTimeType::marshall().
With the default app template configuration, DateTimeType is configured to use locale aware parsing, and since no default locale format is being set, \Cake\I18n\Time::parseDateTime() will parse the values according to its default "to string format" (Time::$_toStringFormat), which defaults to the locale aware [IntlDateFormatter::SHORT, IntlDateFormatter::SHORT].
So, if for example your locale is set to en_US, then the value would be parsed with an expected format of M/d/yy, h:mm a, which your value wouldn't fit, and hence you'd finally end up with null being set for the entity property.
Make the parser use the proper format
tl;dr
In case the format for the jQuery widget is not being used everywhere in your app, you could for example either temporarily set the proper locale format, or disable locale parsing, like
// for time- or date-only comlumn types you'd use 'time' or 'date' instead of 'datetime'
$dateTimeType = Type::build('datetime')->setLocaleFormat('yyyy/MM/dd HH:mm');
// ...
$intervention = $this->Interventions->patchEntity($intervention, $this->request->data);
// ...
$dateTimeType->setLocaleFormat(null);
or
$dateTimeType = Type::build('datetime')->useLocaleParser(false);
// ...
$intervention = $this->Interventions->patchEntity($intervention, $this->request->data);
// ...
$dateTimeType->useLocaleParser(true);
It should be noted that this will affect all date/time input, not just your starttime and endtime fields!
Should the format used by the jQuery widget on the other hand be the format that you wish to use all the way through your app, then changing the default format could do it too, like
use Cake\I18n\Time;
use Cake\I18n\FrozenTime;
// To affect date-only columns you'd configure `Date` and `FrozenDate`.
// For time-only columns, see the linked SO question below.
Time::setToStringFormat('yyyy/MM/dd HH:mm');
FrozenTime::setToStringFormat('yyyy/MM/dd HH:mm');
in your bootstrap.php. Note that there's also Time/FrozenTime::setJsonEncodeFormat() and Time/FrozenTime::$niceFormat which you may want/need to modify too.
See also
Cookbook > Internationalization & Localization > Parsing Localized Datetime Data
Cookbook > Time > Setting the Default Locale and Format String
CakePHP 3 time column gets date added
Convert the input before marshalling it
Another option would be to for example convert the data to Time instances before the marshalling process. This would avoid possible problems with the previous mentioned solution that would affect all input.
In your InterventionsTable class (could also be put in a behavior or an external listener):
use Cake\Event\Event;
use Cake\I18n\Time;
...
public function beforeMarshal(Event $event, \ArrayObject $data, \ArrayObject $options)
{
foreach (['starttime', 'endtime'] as $key) {
if (isset($data[$key]) && is_string($data[$key])) {
$data[$key] = Time::parseDateTime($data[$key], 'yyyy/MM/dd HH:mm');
}
}
}
See also
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities

Related

Does Symfony DateTime validator support Y-m-dTH:i:s?

I need to validate dates in Symfony and the expected format is Y-m-dTH:i:s, for example 2019-08-02T23:09:01
This is how the DateTime object is being instantiated:
//...some code
'start_date' => new DateTime([
'format' => 'Y-m-dTH:i:s'
]),
//...some code
and even though start_date is correct (for example 2019-08-01T20:04:00), the validator still renders this invalid. If I try with 2019-08-01 20:04:00 and a format of 'Y-m-d H:i:s', then it works. Is it possible to use that T in the format?
Whats the origin of the date that you want to validate ? It's from form(user input?) ?
If yes
then you should use DateTime Assert on entity connected to form
https://symfony.com/doc/current/reference/constraints/DateTime.html
also
you can use datetime field (https://symfony.com/doc/current/reference/forms/types/datetime.html) on form to validate on data input
If no
If you have value from (for example) database, and you are not sure if is it vaild
you can use Datetime Assert ( link above) with validating "raw values" , here is example :
https://symfony.com/doc/current/validation/raw_values.html

PHPUnit Form Test Failing on DateTime

I have several unit tests failing on my local machine because DateTime values differ:
4) Tests\AppBundle\Form\AssetStatusTypeTest::testValidForm with data set #0 (array('active'))
Failed asserting that two objects are equal.
--- Expected
+++ Actual
## ##
'status' => 'active'
'type' => null
'amount' => null
- 'createdAt' => 2018-09-20T20:34:47.047520+0200
+ 'createdAt' => 2018-09-20T20:34:47.047870+0200
'updatedAt' => null
'contract' => null
'admin' => null
The test:
public function testValidForm($data)
{
$form = $this->factory->create(AssetStatusType::class);
$object = Entity::fromArray(new Asset(), $data);
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($object, $form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($data) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
Which is more or less copied from the docs.
Now, the createdAt field is set like this:
public function __construct()
{
if (empty($this->createdAt)) {
$this->createdAt = new \DateTime();
}
}
My main point is: This test is passing on our Jenkins. It does not locally on several developer machines. My first impulse was to check ini timezone settings.
I wonder how this is even possible. If I get it right, expected is when object was created and actual is when the form is submitted. So both objects can never have the same createdAt timestamp. Unless precision is probably super low.
The two machines are running different versions of PHP. PHP7.1 (and above) includes microseconds when it creates a DateTime object, but code running on PHP 5.x and 7.0, won't.
There are two ways to work with this:
Don't compare exact DateTimes, but convert them to seconds with $datetimeObj->format('U');. You'll still get occasional test failures when one test is created at say 1.99998 and the next call to create a datetime is at 2.0001, and so the test still fails when converted to seconds.
Use 'Clock Mocking'. Using some interesting PHP namespace tricks, the global time() function is overridden (sleep() too). You have to create new DateTime object to make sure they actually use the new version of the time() function, but the clock would effectively stop - and sleep()'s just turn into something more like $time += $seconds; - which also means that a sleep(3600); takes effective zero time.
The symfony/phpunit-bridge ClockMock.php can be used just as a library if you don't want to include it as a listener in your PHPunit configuration.

LessThanOrEqual Date without time

I try to validate a Date (not DateTime)
in my validator, i have :
myDate:
- Date: ~
- LessThanOrEqual:
value: today
message: "myDate must be less or equal than today."
Before submitting my form, i send this date to my API (i use symfony as an API with FOSRestBundle):
myDate:"2017-06-09T00:00:00.000Z"
But when i look Symfony\Component\Validator\Constraints\LessThanOrEqualValidator in this method :
protected function compareValues($value1, $value2)
{
return $value1 <= $value2;
}
i have these values:
$value1
DateTime::__set_state(array(
'date' => '2017-06-09 02:00:00.000000',
'timezone_type' => 3,
'timezone' => 'Europe/Paris',
))
$value2
DateTime::__set_state(array(
'date' => '2017-06-09 00:00:00.000000',
'timezone_type' => 3,
'timezone' => 'Europe/Paris',
))
And my validation fails.
Can you help me to solve this problem. I don't need time, i just want to validate the Date. How can i remove the 2 hours ?
Thanks
EDIT :
In my php.ini, i have :
date.timezone ="Europe/Paris"
I solved the problem by sending the right time
First of all LessThanOrEqual do just what its names stands for. It compares if left operand is less or equal to the right operand. No matter if it's a \DateTime or int or something else.
Since you validating \DeteTime i'd suggest you to use Callback Constraint or Expression Constraint so you can define how to validate those. My personal choice for this usecase would be the validation via POST_SUBMIT event in you FormType. See - How to add validators on the fly in Symfony2?

Pimcore: Setting DateTime Class Fields on Objects

I'm writing an importer and am getting stuck with actually creating the objects. Specifically, I'm having troubles with DateTime fields. I have a class called blogArticle and I'm using them just as the Pimcore demo data uses them for blog articles.
$newPost = new Object\BlogArticle();
$newPost->setCreationDate( time() );
$newPost->setPublished( true );
$newPost->setText( $text ); // text input field
$newPost->setTitle( $title ); // text input field
$newPost->setDate( $date ); // datetime input field
$newPost->setKey( \Pimcore\File::getValidFilename( $key ) );
$newPost->setParentId( $id );
$newPost->save();
The exact error I am getting is:
Whoops\Exception\ErrorException thrown with message "Call to a member function getTimestamp() on a non-object"
Stacktrace:
#0 Whoops\Exception\ErrorException in /.../pimcore/models/Object/ClassDefinition/Data/Datetime.php:73
I cannot find anything in the documentation apart from how the value is stored in the database for this field type. Literally zero documentation on how to appropriately assign values to class fields per field type.
SOLUTION
Thanks to Igor Benko on solving this one!
$newPost_date = new DateTime( "2016-07-01 12:10:37" );
$newPost->setDate( $newPost_date );
It seems that your system is still set to use the Zend_Date. DateTime is used only if you have a clean install of Pimcore 4, otherwise the compatibility layer is turned on by default after the update. The compatibility layer uses Zend_Date instead.
In your system.php you have to turn the flag useZendDate to false in order to use DateTime class.
You need to pass an instance of DateTime class to the setter. Something like this:
$date=new DateTime("2016-07-01 12:10:37");
$newPost->setDate($date);
See this:
https://www.pimcore.org/wiki/display/PIMCORE4/Update+from+Version+3.x+to+Version+4#UpdatefromVersion3.xtoVersion4-PHP%27sDateTimereplacesZend_Date
EDIT: Updated the answer after #GrafikMatthew updated his question.

Why are date/time values interpreted incorrectly when patching/saving?

I try to save data from a cakephp 3 form. All data are well saved but datetime not. I've got 2 datetime fields. Those fields are filled by jquery-ui widget.
The problem seems to happened when pacthing entity.
$intervention = $this->Interventions->patchEntity($intervention, $this->request->data);
Debug of $this->request->data :
'user_id' => '1',
'description' => 'test',
'starttime' => '2015/11/15 10:00',
'endtime' => '2015/11/15 12:10'
Debug of my object $intervention after pacthEntity :
object(App\Model\Entity\Intervention)
'id' => (int) 3,
'user_id' => (int) 1,
'description' => 'test',
'starttime' => null,
'endtime' => null
...
starttime and endtime become null and I don't understand why.
Is somebody had this pb before ?
I tried (for debuging and understanding) to force fields value afer patching
and datetime fields in mysql are ok.
$intervention->starttime = date('Y-m-d H:i:s', strtotime($this->request->data['starttime']));
$intervention->endtime = date('Y-m-d H:i:s', strtotime($this->request->data['endtime']));
Thanks for help
Date/time values are being casted/parsed in a locale aware fashion
Update: this is the default behavior with the CakePHP application template versions prior to 3.2.5. As of 3.2.5 locale parsing is not enabled by default anymore, which will make the date/time marshalling logic expect a default format of Y-m-d H:i:s instead.
In the marshalling process, values are being "casted" according to the respective column types. For DATETIME columns, this is done by the \Cake\Database\Type\DateTimeType type class.
To be exact, this is done in \Cake\Database\Type\DateTimeType::marshall().
With the default app template configuration, DateTimeType is configured to use locale aware parsing, and since no default locale format is being set, \Cake\I18n\Time::parseDateTime() will parse the values according to its default "to string format" (Time::$_toStringFormat), which defaults to the locale aware [IntlDateFormatter::SHORT, IntlDateFormatter::SHORT].
So, if for example your locale is set to en_US, then the value would be parsed with an expected format of M/d/yy, h:mm a, which your value wouldn't fit, and hence you'd finally end up with null being set for the entity property.
Make the parser use the proper format
tl;dr
In case the format for the jQuery widget is not being used everywhere in your app, you could for example either temporarily set the proper locale format, or disable locale parsing, like
// for time- or date-only comlumn types you'd use 'time' or 'date' instead of 'datetime'
$dateTimeType = Type::build('datetime')->setLocaleFormat('yyyy/MM/dd HH:mm');
// ...
$intervention = $this->Interventions->patchEntity($intervention, $this->request->data);
// ...
$dateTimeType->setLocaleFormat(null);
or
$dateTimeType = Type::build('datetime')->useLocaleParser(false);
// ...
$intervention = $this->Interventions->patchEntity($intervention, $this->request->data);
// ...
$dateTimeType->useLocaleParser(true);
It should be noted that this will affect all date/time input, not just your starttime and endtime fields!
Should the format used by the jQuery widget on the other hand be the format that you wish to use all the way through your app, then changing the default format could do it too, like
use Cake\I18n\Time;
use Cake\I18n\FrozenTime;
// To affect date-only columns you'd configure `Date` and `FrozenDate`.
// For time-only columns, see the linked SO question below.
Time::setToStringFormat('yyyy/MM/dd HH:mm');
FrozenTime::setToStringFormat('yyyy/MM/dd HH:mm');
in your bootstrap.php. Note that there's also Time/FrozenTime::setJsonEncodeFormat() and Time/FrozenTime::$niceFormat which you may want/need to modify too.
See also
Cookbook > Internationalization & Localization > Parsing Localized Datetime Data
Cookbook > Time > Setting the Default Locale and Format String
CakePHP 3 time column gets date added
Convert the input before marshalling it
Another option would be to for example convert the data to Time instances before the marshalling process. This would avoid possible problems with the previous mentioned solution that would affect all input.
In your InterventionsTable class (could also be put in a behavior or an external listener):
use Cake\Event\Event;
use Cake\I18n\Time;
...
public function beforeMarshal(Event $event, \ArrayObject $data, \ArrayObject $options)
{
foreach (['starttime', 'endtime'] as $key) {
if (isset($data[$key]) && is_string($data[$key])) {
$data[$key] = Time::parseDateTime($data[$key], 'yyyy/MM/dd HH:mm');
}
}
}
See also
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities

Resources