The issue I was seeing was that any DateTime data type that was explicitly set to a null was being returned as the Unix Epoch date - January 1, 1970 (midnight UTC/GMT). Since my code was relying on nulls being returned and instead was getting valid DateTime objects, the behaviour of my code was not as expected.
So I started to make my way through php-gds source code. The file of interest in this case was src/GDS/Mapper/ProtoBuf.php. Specifically the extractDatetimeValue() function which was used to return a DateTime object to the client. The function looked like this...
GDS/Mapper/ProtoBuf.php
protected function extractDatetimeValue($obj_property)
{
return \DateTime::createFromFormat(
self::DATETIME_FORMAT_UDOTU,
sprintf('%0.6F', bcdiv($obj_property->getTimestampMicrosecondsValue(), self::MICROSECONDS))
);
}
That got my attention, so what if I replaced all of the constants with their string literals and just tried using 0 as the value for the microseconds? The code then became...
GDS/Mapper/ProtoBuf.php
\DateTime::createFromFormat('U.u', sprintf('%0.6F', bcdiv(0, 1000000)));
Printing out the value of this object using the var_export function then gave this output...
Output
DateTime::__set_state(array( 'date' => '1970-01-01 00:00:00.000000', 'timezone_type' => 1, 'timezone' => '+00:00', ))
That certainly looked familiar! So for some reason the getTimestampMicrosecondsValue() call was returning 0 to php-gds. This function was from the appengine-php-sdk library. The file of interest here was google/appengine/datastore/entity_v4_pb.php. I don't love the way Google mashed all of the classes into a single file here so it took a bit scrolling to get to the place of interest, eventually though I found the Value class and the getTimestampMicrosecondsValue() function...
google/appengine/datastore/entity_v4_pb.php
namespace google\appengine\datastore\v4 {
class Value extends \google\net\ProtocolMessage {
...
public function getTimestampMicrosecondsValue() {
if (!isset($this->timestamp_microseconds_value)) {
return "0";
}
return $this->timestamp_microseconds_value;
}
...
}
Well then! That is obvious! Since the statement `!isset($this->timestamp_microseconds_value)` would return true i.e. the value does not exist because it is set to null, it was returning 0.
This can be validated with this test case...
PHP
$obj_schema = (new GDS\Schema('Test'))->addDatetime('testDate');
$obj_store = new GDS\Store($obj_schema);
$obj_entity = new GDS\Entity();
$obj_entity->testDate = null;
$obj_store->upsert($obj_entity);
foreach ($obj_store->fetchAll() as $entity) {
echo var_export($entity->testDate, true);
}
Since I needed a null, I added a workaround that checked the timestamp value on the DateTime object and if it was set to 0, it reset the entity property to a null. It was not the best way to do this, in fact if the date was actually set to the Epoch start date, this code would incorrectly return a null. Still...this was a workaround and it worked for my case.
PHP Workaround
if ($entity->testDate->getTimestamp() == 0) {
$entity->testDate = null;
}
Now the code from Google does have hasTimestampMicrosecondsValue() function, php-gds just does not use it. So there was a fix that could be added to php-gds in this case. To my surprise however, none of the functions that convert DataStore values to what php-gds sets on its Entity objects have any guard statements that check if the property exists or not.
So after all that I can conclude that this was indeed a bug in the php-gds code! I'll raise an issue ticket (or maybe even a pull request with updates) with Tom Walder in the next couple of days and hopefully he can rectify it in the 4.0 release.
-i