diff --git a/docs/manifest.json b/docs/manifest.json index 7be206e79cdf..7ee6ad385580 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -6,6 +6,7 @@ "defaultService": "servicebuilder", "markdown": "php", "versions": [ + "v0.7.1", "v0.7.0", "v0.6.0", "v0.5.1", diff --git a/src/Datastore/Blob.php b/src/Datastore/Blob.php new file mode 100644 index 000000000000..d55013e56d31 --- /dev/null +++ b/src/Datastore/Blob.php @@ -0,0 +1,79 @@ +blob(file_get_contents(__DIR__ .'/family-photo.jpg')); + * ``` + */ +class Blob +{ + /** + * @var mixed + */ + private $value; + + /** + * Create a blob + * + * @param string|resource|StreamInterface $value The blob value + */ + public function __construct($value) + { + $this->value = Psr7\stream_for($value); + } + + /** + * Get the blob contents as a stream + * + * Example: + * ``` + * $value = $blob->value(); + * ``` + * + * @returns StreamInterface + */ + public function get() + { + return $this->value; + } + + /** + * Cast the blob to a string + * + * Example: + * ``` + * echo (string) $blob->value(); + * ``` + * + * @return string + */ + public function __toString() + { + return (string) $this->value; + } +} diff --git a/src/Datastore/DatastoreClient.php b/src/Datastore/DatastoreClient.php index 4495659ffb32..30593baf6faf 100644 --- a/src/Datastore/DatastoreClient.php +++ b/src/Datastore/DatastoreClient.php @@ -311,6 +311,46 @@ public function entity($key, array $entity = [], array $options = []) return $this->operation->entity($key, $entity, $options); } + /** + * Create a new GeoPoint + * + * Example: + * ``` + * $geoPoint = $datastore->geoPoint(37.4220, -122.0841); + * ``` + * + * @see https://cloud.google.com/datastore/reference/rest/Shared.Types/LatLng LatLng + * + * @param float $latitude The latitude + * @param float $longitude The longitude + * @return GeoPoint + */ + public function geoPoint($latitude, $longitude) + { + return new GeoPoint($latitude, $longitude); + } + + /** + * Create a new Blob + * + * Example: + * ``` + * $blob = $datastore->blob('hello world'); + * ``` + * + * ``` + * // Blobs can be used to store binary data + * $blob = $datastore->blob(file_get_contents(__DIR__ .'/family-photo.jpg')); + * ``` + * + * @param string|resource|StreamInterface $value + * @return Blob + */ + public function blob($value) + { + return new Blob($value); + } + /** * Allocates an available ID to a given incomplete key * diff --git a/src/Datastore/Entity.php b/src/Datastore/Entity.php index a13b70cc3fce..b69d88419de9 100644 --- a/src/Datastore/Entity.php +++ b/src/Datastore/Entity.php @@ -27,6 +27,26 @@ * Entity implements PHP's [ArrayAccess](http://php.net/arrayaccess), allowing * access via the array syntax (example below). * + * Properties are mapped automatically to their corresponding Datastore value + * types. Refer to the table below for a guide to how types are stored. + * + * | **PHP Type** | **Datastore Value Type** | + * |--------------------------------------------|--------------------------------------| + * | `\DateTimeInterface` | `timestampValue` | + * | {@see Google\Cloud\Datastore\Key} | `keyValue` | + * | {@see Google\Cloud\Datastore\GeoPoint} | `geoPointValue` | + * | {@see Google\Cloud\Datastore\Entity} | `entityValue` | + * | {@see Google\Cloud\Datastore\Blob} | `blobValue` | + * | Associative Array | `entityValue` (No Key) | + * | Non-Associative Array | `arrayValue` | + * | `float` | `doubleValue` | + * | `int` | `integerValue` | + * | `string` | `stringValue` | + * | `resource` | `blobValue` | + * | `NULL` | `nullValue` | + * | `bool` | `booleanValue` | + * | `object` (Outside types specified above) | **ERROR** `InvalidArgumentException` | + * * Example: * ``` * use Google\Cloud\ServiceBuilder; diff --git a/src/Datastore/EntityMapper.php b/src/Datastore/EntityMapper.php index bc2ea64d4725..89ecbd9ef9e6 100644 --- a/src/Datastore/EntityMapper.php +++ b/src/Datastore/EntityMapper.php @@ -81,8 +81,6 @@ public function responseToExcludeFromIndexes(array $entityData) $excludes = []; foreach ($entityData as $key => $property) { - $type = key($property); - if (isset($property['excludeFromIndexes']) && $property['excludeFromIndexes']) { $excludes[] = $key; } @@ -205,6 +203,15 @@ public function convertValue($type, $value) break; + case 'blobValue': + if ($this->isEncoded($value)) { + $value = base64_decode($value); + } + + $result = new Blob($value); + + break; + default: $result = $value; break; @@ -316,6 +323,15 @@ public function valueObject($value, $exclude = false) public function objectProperty($value) { switch (true) { + case $value instanceof Blob: + return [ + 'blobValue' => ($this->encode) + ? base64_encode((string) $value) + : (string) $value + ]; + + break; + case $value instanceof \DateTimeInterface: return [ 'timestampValue' => $value->format(\DateTime::RFC3339) @@ -323,9 +339,9 @@ public function objectProperty($value) break; - case $value instanceof Key: + case $value instanceof Entity: return [ - 'keyValue' => $value->keyObject() + 'entityValue' => $this->objectToRequest($value) ]; break; @@ -337,11 +353,13 @@ public function objectProperty($value) break; - case $value instanceof Entity: + case $value instanceof Key: return [ - 'entityValue' => $this->objectToRequest($value) + 'keyValue' => $value->keyObject() ]; + break; + default: throw new InvalidArgumentException( sprintf('Value of type `%s` could not be serialized', get_class($value)) @@ -390,4 +408,25 @@ private function convertArrayToEntityValue(array $value) ] ]; } + + private function isEncoded($value) + { + // Check if there are valid base64 characters + if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $value)) { + return false; + } + + // Decode the string in strict mode and check the results + $decoded = base64_decode($value, true); + if ($decoded == false) { + return false; + } + + // Encode the string again + if (base64_encode($decoded) != $value) { + return false; + } + + return true; + } } diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index 2683188e62a2..778738ddc11b 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -46,7 +46,7 @@ */ class ServiceBuilder { - const VERSION = '0.7.0'; + const VERSION = '0.7.1'; /** * @var array Configuration options to be used between clients. diff --git a/tests/Datastore/BlobTest.php b/tests/Datastore/BlobTest.php new file mode 100644 index 000000000000..469f0a06f08d --- /dev/null +++ b/tests/Datastore/BlobTest.php @@ -0,0 +1,56 @@ +assertEquals('hello world', (string) $blob); + } + + public function testBlobResource() + { + $string = 'hello world'; + $stream = fopen('php://memory','r+'); + fwrite($stream, $string); + rewind($stream); + + $blob = new Blob($stream); + $this->assertEquals('hello world', (string) $blob); + } + + public function testBlobStreamInterface() + { + $blob = new Blob(Psr7\stream_for('hello world')); + $this->assertEquals('hello world', (string) $blob); + } + + public function testToString() + { + $blob = new Blob('hello world'); + $this->assertEquals((string)$blob->get(), (string) $blob); + } +} diff --git a/tests/Datastore/DatastoreClientTest.php b/tests/Datastore/DatastoreClientTest.php index f444ce4cd77a..5f95411e85fd 100644 --- a/tests/Datastore/DatastoreClientTest.php +++ b/tests/Datastore/DatastoreClientTest.php @@ -17,9 +17,11 @@ namespace Google\Cloud\Tests\Datastore; +use Google\Cloud\Datastore\Blob; use Google\Cloud\Datastore\Connection\ConnectionInterface; use Google\Cloud\Datastore\DatastoreClient; use Google\Cloud\Datastore\Entity; +use Google\Cloud\Datastore\GeoPoint; use Google\Cloud\Datastore\Key; use Google\Cloud\Datastore\Operation; use Google\Cloud\Datastore\Query\GqlQuery; @@ -144,6 +146,23 @@ public function testEntity() $this->assertEquals($entity['foo'], 'bar'); } + public function testBlob() + { + $blob = $this->datastore->blob('foo'); + $this->assertInstanceOf(Blob::class, $blob); + $this->assertEquals('foo', (string) $blob); + } + + public function testGeoPoint() + { + $point = $this->datastore->geoPoint(1.1, 0.1); + $this->assertInstanceOf(GeoPoint::class, $point); + $this->assertEquals($point->point(), [ + 'latitude' => 1.1, + 'longitude' => 0.1 + ]); + } + public function testAllocateId() { $datastore = new DatastoreClientStubNoService; diff --git a/tests/Datastore/EntityMapperTest.php b/tests/Datastore/EntityMapperTest.php index d79c23b6694d..caa4f19f8fe5 100644 --- a/tests/Datastore/EntityMapperTest.php +++ b/tests/Datastore/EntityMapperTest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Tests\Datastore; +use Google\Cloud\Datastore\Blob; use Google\Cloud\Datastore\Entity; use Google\Cloud\Datastore\EntityMapper; use Google\Cloud\Datastore\GeoPoint; @@ -34,6 +35,40 @@ public function setUp() $this->mapper = new EntityMapper('foo', true); } + public function testResponseToProperties() + { + $data = [ + 'foo' => [ + 'stringValue' => 'bar' + ], + 'dubs' => [ + 'doubleValue' => 1.1 + ] + ]; + + $res = $this->mapper->responseToProperties($data); + + $this->assertEquals('bar', $res['foo']); + $this->assertEquals(1.1, $res['dubs']); + } + + public function testResponseToExcludedProperties() + { + $data = [ + 'foo' => [ + 'stringValue' => 'bar', + 'excludeFromIndexes' => true + ], + 'dubs' => [ + 'doubleValue' => 1.1 + ] + ]; + + $res = $this->mapper->responseToExcludeFromIndexes($data); + + $res = $this->assertEquals(['foo'], $res); + } + public function testObjectToRequest() { $key = new Key('foo', [ @@ -190,6 +225,28 @@ public function testConvertValueInteger() $this->assertEquals(1, $res); } + public function testConvertValueBlob() + { + $type = 'blobValue'; + $val = base64_encode('hello world'); + + $res = $this->mapper->convertValue($type, $val); + $this->assertInstanceOf(Blob::class, $res); + + $this->assertEquals('hello world', (string)$res); + } + + public function testConvertValueBlobNotEncoded() + { + $type = 'blobValue'; + $val = 'hello world'; + + $res = $this->mapper->convertValue($type, $val); + $this->assertInstanceOf(Blob::class, $res); + + $this->assertEquals('hello world', (string)$res); + } + public function testArrayValue() { $type = 'arrayValue'; @@ -299,6 +356,22 @@ public function testValueExcludeFromIndexes() $this->assertFalse(isset($res['excludeFromIndexes'])); } + public function testObjectPropertyBlob() + { + $res = $this->mapper->valueObject(new Blob('hello world')); + + $this->assertEquals('hello world', base64_decode($res['blobValue'])); + } + + public function testObjectPropertyBlobNotEncoded() + { + $mapper = new EntityMapper('foo', false); + + $res = $mapper->valueObject(new Blob('hello world')); + + $this->assertEquals('hello world', $res['blobValue']); + } + public function testObjectPropertyDateTime() { $res = $this->mapper->valueObject(new \DateTimeImmutable); diff --git a/tests/Datastore/OperationTest.php b/tests/Datastore/OperationTest.php index c88f906f26c7..e4f1317c32e8 100644 --- a/tests/Datastore/OperationTest.php +++ b/tests/Datastore/OperationTest.php @@ -558,6 +558,53 @@ public function testMapEntityResult() $this->assertEquals($entity['found'][0]->prop, $res[0]['entity']['properties']['prop']['stringValue']); } + public function testMapEntityResultArrayOfClassNames() + { + $res = json_decode(file_get_contents(__DIR__ .'/../fixtures/datastore/entity-result.json'), true); + + $this->connection->lookup(Argument::type('array')) + ->willReturn([ + 'found' => $res + ]); + + $this->operation->setConnection($this->connection->reveal()); + + $k = $this->prophesize(Key::class); + $k->state()->willReturn(Key::STATE_COMPLETE); + + $entity = $this->operation->lookup([$k->reveal()], [ + 'className' => [ + 'Kind' => MyEntity::class + ] + ]); + + $this->assertInstanceOf(MyEntity::class, $entity['found'][0]); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testMapEntityResultArrayOfClassNamesMissingKindMapItem() + { + $res = json_decode(file_get_contents(__DIR__ .'/../fixtures/datastore/entity-result.json'), true); + + $this->connection->lookup(Argument::type('array')) + ->willReturn([ + 'found' => $res + ]); + + $this->operation->setConnection($this->connection->reveal()); + + $k = $this->prophesize(Key::class); + $k->state()->willReturn(Key::STATE_COMPLETE); + + $entity = $this->operation->lookup([$k->reveal()], [ + 'className' => [ + 'Kind2' => MyEntity::class + ] + ]); + } + public function testTransactionInReadOptions() { $this->connection->lookup(Argument::that(function ($arg) {