Igor Kromin |   Consultant. Coder. Blogger. Tinkerer. Gamer.

Whenever I come across what looks like a bug in the 3rd party library I'm using, I try to see what it's doing before assuming that there's a bug. Sometimes the process is as simple as looking at the library's code on GitHub, other times it involves decompiling a library and manually editing its files with a hex editor. Recently I've come across what looked like an issue in the php-gds library used to access the Google Cloud DataStore in PHP.

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

A quick disclaimer...

Although I put in a great effort into researching all the topics I cover, mistakes can happen. Use of any information from my blog posts should be at own risk and I do not hold any liability towards any information misuse or damages caused by following any of my posts.

All content and opinions expressed on this Blog are my own and do not represent the opinions of my employer (Oracle). Use of any information contained in this blog post/article is subject to this disclaimer.
Hi! You can search my blog here ⤵
NOTE: (2022) This Blog is no longer maintained and I will not be answering any emails or comments.

I am now focusing on Atari Gamer.