diff --git a/.gitignore b/.gitignore
index aae4db8a3601..ff6640ed343f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,5 @@
build/
composer.phar
composer.lock
-docs/json/**/*.json
+docs/json/*
vendor/
diff --git a/.travis.yml b/.travis.yml
index 01f68113bc39..0c37a6d81b32 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,27 +1,31 @@
language: php
-
sudo: required
dist: trusty
matrix:
- include:
- - php: 5.5.38
- - php: 5.6.25
- - php: 7.0
- - php: 7.1
- - php: hhvm
- group: edge
- fast_finish: true
+ include:
+ - php: 5.5.38
+ - php: 5.6.25
+ - php: 7.0
+ - php: 7.1
+ - php: hhvm
+ group: edge
+ fast_finish: true
before_script:
- - pecl install grpc || echo 'Failed to install grpc'
- - composer install
+ - pecl install grpc || echo 'Failed to install grpc'
+ - composer install
script:
- - ./dev/sh/tests
- - vendor/bin/phpcs --standard=./phpcs-ruleset.xml
- - ./dev/sh/build-docs
+ - ./dev/sh/tests
+ - vendor/bin/phpcs --standard=./phpcs-ruleset.xml
+ - ./dev/sh/build-docs
after_success:
- - bash <(curl -s https://codecov.io/bash)
- - ./dev/sh/push-docs
+ - bash <(curl -s https://codecov.io/bash)
+ - ./dev/sh/push-docs
+ - ./dev/sh/trigger-split
+ - cat ./build/snippets-uncovered.json
+
+after_failure:
+ - echo "SNIPPET COVERAGE REPORT" && cat ./build/snippets-uncovered.json
diff --git a/README.md b/README.md
index 0c6a91082dfb..43ea2d961b1b 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@ This client supports the following Google Cloud Platform services at a [Beta](#v
* [Google Stackdriver Logging](#google-stackdriver-logging-beta) (Beta)
* [Google Cloud Datastore](#google-cloud-datastore-beta) (Beta)
* [Google Cloud Storage](#google-cloud-storage-beta) (Beta)
+* [Google Cloud Vision](#google-cloud-vision-beta) (Beta)
This client supports the following Google Cloud Platform services at an [Alpha](#versioning) quality level:
* [Google Cloud Natural Language](#google-cloud-natural-language-alpha) (Alpha)
* [Google Cloud Pub/Sub](#google-cloud-pubsub-alpha) (Alpha)
* [Google Cloud Speech](#google-cloud-speech-alpha) (Alpha)
* [Google Cloud Translation](#google-cloud-translation-alpha) (Alpha)
-* [Google Cloud Vision](#google-cloud-vision-alpha) (Alpha)
If you need support for other Google APIs, please check out the [Google APIs Client Library for PHP](https://github.com/google/google-api-php-client).
@@ -61,6 +61,14 @@ foreach ($queryResults->rows() as $row) {
}
```
+#### google/cloud-bigquery
+
+Google BigQuery can be installed separately by requiring the `google/cloud-bigquery` composer package:
+
+```
+$ require google/cloud-bigquery
+```
+
## Google Stackdriver Logging (Beta)
- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/logging/loggingclient)
@@ -93,6 +101,14 @@ foreach ($entries as $entry) {
}
```
+#### google/cloud-logging
+
+Google Stackdriver Logging can be installed separately by requiring the `google/cloud-logging` composer package:
+
+```
+$ require google/cloud-logging
+```
+
## Google Cloud Datastore (Beta)
- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/datastore/datastoreclient)
@@ -124,6 +140,14 @@ $key = $datastore->key('Person', '12345328897844');
$entity = $datastore->lookup($key);
```
+#### google/cloud-datastore
+
+Google Cloud Datastore can be installed separately by requiring the `google/cloud-datastore` composer package:
+
+```
+$ require google/cloud-datastore
+```
+
## Google Cloud Storage (Beta)
- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/storage/storageclient)
@@ -152,6 +176,69 @@ $object = $bucket->object('file_backup.txt');
$object->downloadToFile('/data/file_backup.txt');
```
+#### Stream Wrapper
+
+```php
+require 'vendor/autoload.php';
+
+use Google\Cloud\Storage\StorageClient;
+
+$storage = new StorageClient([
+ 'projectId' => 'my_project'
+]);
+$storage->registerStreamWrapper();
+
+$contents = file_get_contents('gs://my_bucket/file_backup.txt');
+```
+
+#### google/cloud-storage
+
+Google Cloud Storage can be installed separately by requiring the `google/cloud-storage` composer package:
+
+```
+$ require google/cloud-storage
+```
+
+## Google Cloud Vision (Beta)
+
+- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/vision/visionclient)
+- [Official Documentation](https://cloud.google.com/vision/docs)
+
+#### Preview
+
+```php
+require 'vendor/autoload.php';
+
+use Google\Cloud\Vision\VisionClient;
+
+$vision = new VisionClient([
+ 'projectId' => 'my_project'
+]);
+
+// Annotate an image, detecting faces.
+$image = $vision->image(
+ fopen('/data/family_photo.jpg', 'r'),
+ ['faces']
+);
+
+$annotation = $vision->annotate($image);
+
+// Determine if the detected faces have headwear.
+foreach ($annotation->faces() as $key => $face) {
+ if ($face->hasHeadwear()) {
+ echo "Face $key has headwear.\n";
+ }
+}
+```
+
+#### google/cloud-vision
+
+Google Cloud Vision can be installed separately by requiring the `google/cloud-vision` composer package:
+
+```
+$ require google/cloud-vision
+```
+
## Google Cloud Translation (Alpha)
- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/translate/translateclient)
@@ -198,6 +285,14 @@ foreach ($languages as $language) {
}
```
+#### google/cloud-translate
+
+Google Cloud Translation can be installed separately by requiring the `google/cloud-translate` composer package:
+
+```
+$ require google/cloud-translate
+```
+
## Google Cloud Natural Language (Alpha)
- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/naturallanguage/naturallanguageclient)
@@ -237,6 +332,14 @@ foreach ($tokens as $token) {
}
```
+#### google/cloud-natural-language
+
+Google Cloud Natural Language can be installed separately by requiring the `google/cloud-natural-language` composer package:
+
+```
+$ require google/cloud-natural-language
+```
+
## Google Cloud Pub/Sub (Alpha)
- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/pubsub/pubsubclient)
@@ -276,6 +379,14 @@ foreach ($messages as $message) {
}
```
+#### google/cloud-pubsub
+
+Google Cloud Pub/Sub can be installed separately by requiring the `google/cloud-pubsub` composer package:
+
+```
+$ require google/cloud-pubsub
+```
+
## Google Cloud Speech (Alpha)
- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/speech/speechclient)
@@ -303,36 +414,12 @@ foreach ($results as $result) {
}
```
-## Google Cloud Vision (Alpha)
+#### google/cloud-speech
-- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/vision/visionclient)
-- [Official Documentation](https://cloud.google.com/vision/docs)
-
-#### Preview
-
-```php
-require 'vendor/autoload.php';
-
-use Google\Cloud\Vision\VisionClient;
+Google Cloud Speech can be installed separately by requiring the `google/cloud-speech` composer package:
-$vision = new VisionClient([
- 'projectId' => 'my_project'
-]);
-
-// Annotate an image, detecting faces.
-$image = $vision->image(
- fopen('/data/family_photo.jpg', 'r'),
- ['faces']
-);
-
-$annotation = $vision->annotate($image);
-
-// Determine if the detected faces have headwear.
-foreach ($annotation->faces() as $key => $face) {
- if ($face->hasHeadwear()) {
- echo "Face $key has headwear.\n";
- }
-}
+```
+$ require google/cloud-speech
```
## Caching Access Tokens
diff --git a/composer.json b/composer.json
index 8e2b924f3f15..00dc2f0e8001 100644
--- a/composer.json
+++ b/composer.json
@@ -55,8 +55,8 @@
"james-heinrich/getid3": "^1.9",
"erusev/parsedown": "^1.6",
"vierbergenlars/php-semver": "^3.0",
- "google/proto-client-php": "^0.7",
- "google/gax": "^0.6"
+ "google/proto-client-php": "^0.9",
+ "google/gax": "^0.8"
},
"suggest": {
"google/gax": "Required to support gRPC",
@@ -72,9 +72,18 @@
"psr-4": {
"Google\\Cloud\\Dev\\": "dev/src",
"Google\\Cloud\\Tests\\System\\": "tests/system"
- }
+ },
+ "files": ["dev/src/Functions.php"]
},
"scripts": {
"google-cloud": "dev/google-cloud"
+ },
+ "extra": {
+ "component": {
+ "id": "google-cloud",
+ "target": "git@github.com:jdpedrie-gcp/google-cloud-php.git",
+ "path": "src",
+ "entry": "ServiceBuilder.php"
+ }
}
}
diff --git a/dev/google-cloud b/dev/google-cloud
index cd0636602a4f..e004b499d438 100755
--- a/dev/google-cloud
+++ b/dev/google-cloud
@@ -20,6 +20,7 @@ require __DIR__ . '/../vendor/autoload.php';
use Google\Cloud\Dev\DocGenerator\Command\Docs;
use Google\Cloud\Dev\Release\Command\Release;
+use Google\Cloud\Dev\Split\Command\Split;
use Symfony\Component\Console\Application;
if (!class_exists(Application::class)) {
@@ -33,4 +34,5 @@ if (!class_exists(Application::class)) {
$app = new Application;
$app->add(new Release(__DIR__));
$app->add(new Docs(__DIR__));
+$app->add(new Split(__DIR__));
$app->run();
diff --git a/dev/sh/build-docs b/dev/sh/build-docs
index cc09be67d534..1079b9de9dd6 100755
--- a/dev/sh/build-docs
+++ b/dev/sh/build-docs
@@ -5,7 +5,13 @@ set -ev
function buildDocs () {
echo "doc dir before generation:"
find docs
- composer google-cloud docs
+
+ if [ -z "$TRAVIS_TAG" ]; then
+ ./dev/google-cloud docs
+ else
+ ./dev/google-cloud docs -r ${TRAVIS_TAG}
+ fi
+
echo "doc dir after generation:"
find docs
}
diff --git a/dev/sh/compile-splitsh b/dev/sh/compile-splitsh
new file mode 100755
index 000000000000..1cef5094d2b0
--- /dev/null
+++ b/dev/sh/compile-splitsh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+mkdir $
+export GOPATH=$TRAVIS_BUILD_DIR/go
+
+go get -d github.com/libgit2/git2go
+cd $GOPATH/src/github.com/libgit2/git2go
+git checkout next
+git submodule update --init
+make install
+
+go get github.com/splitsh/lite
+go build -o $TRAVIS_BUILD_DIR/splitsh-lite github.com/splitsh/lite
diff --git a/dev/sh/push-docs b/dev/sh/push-docs
index a0ef3d50e770..0ee349729324 100755
--- a/dev/sh/push-docs
+++ b/dev/sh/push-docs
@@ -2,20 +2,11 @@
set -ev
-function generateDocs () {
- echo "doc dir before generation:"
- find docs
- composer google-cloud docs
- echo "doc dir after generation:"
- find docs
-}
-
function pushDocs () {
- git submodule add -q -f -b gh-pages https://${GH_OAUTH_TOKEN}@github.com/${GH_OWNER}/${GH_PROJECT_NAME} ghpages
- mkdir -p ghpages/json/${1}
- cp -R docs/json/master/* ghpages/json/${1}
- cp docs/overview.html ghpages/json/${1}
- cp docs/toc.json ghpages/json/${1}
+ git submodule add -q -f -b gh-pages https://${GH_OAUTH_TOKEN}@github.com/${TRAVIS_REPO_SLUG} ghpages
+
+ rsync -aP docs/json/* ghpages/json/
+
cp docs/home.html ghpages/json
cp docs/manifest.json ghpages
cd ghpages
@@ -25,7 +16,7 @@ function pushDocs () {
git config user.email "travis@travis-ci.org"
git commit -m "Updating docs for ${1}"
git status
- git push -q https://${GH_OAUTH_TOKEN}@github.com/${GH_OWNER}/${GH_PROJECT_NAME} HEAD:gh-pages
+ git push -q https://${GH_OAUTH_TOKEN}@github.com/${TRAVIS_REPO_SLUG} HEAD:gh-pages
else
echo "Nothing to commit."
fi
diff --git a/dev/sh/split b/dev/sh/split
new file mode 100755
index 000000000000..6ff7c259f9ac
--- /dev/null
+++ b/dev/sh/split
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
+
+SHA=`$TRAVIS_BUILD_DIR/splitsh-lite --prefix=$1`
+git push -q \
+ "https://${GH_OAUTH_TOKEN}@github.com/$2" \
+ $SHA:master --force
diff --git a/dev/sh/trigger-split b/dev/sh/trigger-split
new file mode 100755
index 000000000000..5685a6fe239e
--- /dev/null
+++ b/dev/sh/trigger-split
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+if [[ "$TRAVIS_JOB_NUMBER" == *.1 && -n "$TRAVIS_TAG" ]]; then
+ $(dirname $0)/compile-splitsh
+ git fetch --unshallow
+ composer google-cloud split
+else
+ echo "Split occurs only in a tag run, and in the first matrix build"
+fi
diff --git a/dev/src/DocGenerator/Command/Docs.php b/dev/src/DocGenerator/Command/Docs.php
index 33c522682c84..f4609b8a44a3 100644
--- a/dev/src/DocGenerator/Command/Docs.php
+++ b/dev/src/DocGenerator/Command/Docs.php
@@ -19,7 +19,9 @@
use Google\Cloud\Dev\DocGenerator\DocGenerator;
use Google\Cloud\Dev\DocGenerator\GuideGenerator;
+use Google\Cloud\Dev\DocGenerator\TableOfContents;
use Google\Cloud\Dev\DocGenerator\TypeGenerator;
+use Google\Cloud\Dev\GetComponentsTrait;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RecursiveRegexIterator;
@@ -27,11 +29,17 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Docs extends Command
{
- const DEFAULT_OUTPUT_DIR = 'docs/json/master';
+ use GetComponentsTrait;
+
+ const DEFAULT_OUTPUT_DIR = 'docs/json';
+ const TOC_SOURCE_DIR = 'docs/contents';
+ const TOC_TEMPLATE = 'docs/toc.json';
+ const OVERVIEW_FILE = 'docs/overview.html';
const DEFAULT_SOURCE_DIR = 'src';
private $cliBasePath;
@@ -47,29 +55,121 @@ protected function configure()
{
$this->setName('docs')
->setDescription('Generate Documentation')
- ->addArgument('source', InputArgument::OPTIONAL, 'The source directory to traverse and parse')
- ->addArgument('output', InputArgument::OPTIONAL, 'The directory to output files into');
+ ->addOption('release', 'r', InputOption::VALUE_REQUIRED, 'If set, docs will be generated into tag folders' .
+ ' such as v1.0.0 rather than master.', false)
+ ->addOption('pretty', 'p', InputOption::VALUE_OPTIONAL, 'If set, json files will be written with pretty'.
+ ' formatting using PHP\'s JSON_PRETTY_PRINT flag', false);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
+ $release = ($input->getOption('release') === false && $input->getOption('release') !== 'false')
+ ? null
+ : $input->getOption('release');
+
+ $pretty = ($input->getOption('pretty') === false) ? false : true;
+
$paths = [
- 'source' => ($input->getArgument('source'))
- ? $this->cliBasePath .'/../'. $input->getArgument('source')
- : $this->cliBasePath .'/../'. self::DEFAULT_SOURCE_DIR,
+ 'source' => $this->cliBasePath .'/../'. self::DEFAULT_SOURCE_DIR,
+ 'output' => $this->cliBasePath .'/../'. self::DEFAULT_OUTPUT_DIR,
+ 'project' => $this->cliBasePath .'/../',
+ 'manifest' => $this->cliBasePath .'/../docs/manifest.json',
+ 'toc' => $this->cliBasePath .'/../'. self::TOC_SOURCE_DIR,
+ 'tocTemplate' => $this->cliBasePath .'/../'. self::TOC_TEMPLATE,
+ 'overview' => $this->cliBasePath .'/../'. self::OVERVIEW_FILE
+ ];
+
+ $components = $this->getComponents($paths['source']);
+ $tocTemplate = json_decode(file_get_contents($paths['tocTemplate']), true);
- 'output' => ($input->getArgument('output'))
- ? $this->cliBasePath .'/../'. $input->getArgument('output')
- : $this->cliBasePath .'/../'. self::DEFAULT_OUTPUT_DIR
+ foreach ($components as $component) {
+ $input = $paths['project'] . $component['path'];
+ $source = $this->getFilesList($input);
+ $this->generateComponentDocumentation(
+ $output,
+ $source,
+ $component,
+ $paths,
+ $tocTemplate,
+ $release,
+ $pretty
+ );
+ }
+
+ $source = $this->getFilesList($paths['project'] . '/src');
+ $component = [
+ 'id' => 'google-cloud',
+ 'path' => 'src/'
];
- $types = new TypeGenerator($paths['output']);
+ $this->generateComponentDocumentation(
+ $output,
+ $source,
+ $component,
+ $paths,
+ $tocTemplate,
+ $release,
+ $pretty,
+ false
+ );
+ }
+
+ private function generateComponentDocumentation(
+ OutputInterface $output,
+ array $source,
+ array $component,
+ array $paths,
+ $tocTemplate,
+ $release = false,
+ $pretty = false,
+ $isComponent = true
+ ) {
+ $output->writeln(sprintf('Writing documentation for %s', $component['id']));
+ $output->writeln('--------------');
+
+ $version = $this->getComponentVersion($paths['manifest'], $component['id']);
+
+ $outputPath = ($release)
+ ? $paths['output'] .'/'. $component['id'] .'/'. $version
+ : $paths['output'] .'/'. $component['id'] .'/master';
+
+ $output->writeln(sprintf('Writing to %s', realpath($outputPath)));
+
+ $types = new TypeGenerator($outputPath);
+
+ $docs = new DocGenerator(
+ $types,
+ $source,
+ $outputPath,
+ $this->cliBasePath,
+ $component['id'],
+ $paths['manifest'],
+ $release,
+ $isComponent
+ );
+
+ $docs->generate($component['path'], $pretty);
+
+ $types->write($pretty);
+
+ $output->writeln(sprintf('Writing table of contents to %s', realpath($outputPath)));
+ $contents = json_decode(file_get_contents($paths['toc'] .'/'. $component['id'] .'.json'), true);
+
+ $toc = new TableOfContents(
+ $tocTemplate,
+ $component['id'],
+ $this->getComponentVersion($paths['manifest'], 'google-cloud'),
+ $paths['toc'],
+ $outputPath,
+ $release
+ );
+ $toc->generate($pretty);
- $sourceFiles = $this->getFilesList($paths['source']);
- $docs = new DocGenerator($types, $sourceFiles, $paths['output'], $this->cliBasePath);
- $docs->generate();
+ $output->writeln(sprintf('Copying overview.html to %s', realpath($outputPath)));
+ copy($paths['overview'], $outputPath .'/overview.html');
- $types->write();
+ $output->writeln(' ');
+ $output->writeln(' ');
}
private function getFilesList($source)
diff --git a/dev/src/DocGenerator/DocGenerator.php b/dev/src/DocGenerator/DocGenerator.php
index 7394002af896..5d1736a96127 100644
--- a/dev/src/DocGenerator/DocGenerator.php
+++ b/dev/src/DocGenerator/DocGenerator.php
@@ -32,16 +32,32 @@ class DocGenerator
private $files;
private $outputPath;
private $executionPath;
+ private $componentId;
+ private $manifestPath;
+ private $release;
+ private $isComponent;
/**
* @param array $files
*/
- public function __construct(TypeGenerator $types, array $files, $outputPath, $executionPath)
- {
+ public function __construct(
+ TypeGenerator $types,
+ array $files,
+ $outputPath,
+ $executionPath,
+ $componentId,
+ $manifestPath,
+ $release,
+ $isComponent = true
+ ) {
$this->types = $types;
$this->files = $files;
$this->outputPath = $outputPath;
$this->executionPath = $executionPath;
+ $this->componentId = $componentId;
+ $this->manifestPath = $manifestPath;
+ $this->release = $release;
+ $this->isComponent = $isComponent;
}
/**
@@ -49,31 +65,59 @@ public function __construct(TypeGenerator $types, array $files, $outputPath, $ex
*
* @return void
*/
- public function generate()
+ public function generate($basePath, $pretty)
{
foreach ($this->files as $file) {
- $currentFile = substr(str_replace($this->executionPath, '', $file), 3);
+ if ($basePath) {
+ $currentFileArr = explode($basePath, trim($file, '/'));
+ if (isset($currentFileArr[1])) {
+ $currentFile = trim($currentFileArr[1], '/');
+ }
+ }
+
$isPhp = strrpos($file, '.php') == strlen($file) - strlen('.php');
if ($isPhp) {
$fileReflector = new FileReflector($file);
- $parser = new CodeParser($file, $currentFile, $fileReflector);
+ $parser = new CodeParser(
+ $file,
+ $currentFile,
+ $fileReflector,
+ dirname($this->executionPath),
+ $this->componentId,
+ $this->manifestPath,
+ $this->release,
+ $this->isComponent
+ );
} else {
$content = file_get_contents($file);
- $parser = new MarkdownParser($currentFile, $content);
+ $split = explode('src/', $file);
+ $parser = new MarkdownParser($split[1], $content);
}
$document = $parser->parse();
- $writer = new Writer(json_encode($document), $this->outputPath);
- $writer->write(substr($currentFile, 4));
+ $writer = new Writer($document, $this->outputPath, $pretty);
+ $writer->write($currentFile);
$this->types->addType([
'id' => $document['id'],
'title' => $document['title'],
- 'contents' => $document['id'] . '.json'
+ 'contents' => ($this->isComponent)
+ ? $this->prune($document['id'] . '.json')
+ : $document['id'] . '.json'
]);
}
}
+
+ private function prune($contentsFileName)
+ {
+ $explode = explode('/', $contentsFileName);
+ if (count($explode) > 1) {
+ array_shift($explode);
+ }
+
+ return implode('/', $explode);
+ }
}
diff --git a/dev/src/DocGenerator/Parser/CodeParser.php b/dev/src/DocGenerator/Parser/CodeParser.php
index cbc6616b461e..a88ed87a166c 100644
--- a/dev/src/DocGenerator/Parser/CodeParser.php
+++ b/dev/src/DocGenerator/Parser/CodeParser.php
@@ -18,26 +18,52 @@
namespace Google\Cloud\Dev\DocGenerator\Parser;
use Google\Cloud\Dev\DocBlockStripSpaces;
+use Google\Cloud\Dev\GetComponentsTrait;
use phpDocumentor\Reflection\DocBlock;
+use phpDocumentor\Reflection\DocBlock\Context;
use phpDocumentor\Reflection\DocBlock\Description;
use phpDocumentor\Reflection\DocBlock\Tag\SeeTag;
use phpDocumentor\Reflection\FileReflector;
class CodeParser implements ParserInterface
{
+ use GetComponentsTrait;
+
const SNIPPET_NAME_REGEX = '/\/\/\s?\[snippet\=(\w{0,})\]/';
+ private static $composerFiles = [];
+
private $path;
- private $outputName;
+ private $fileName;
private $reflector;
private $markdown;
-
- public function __construct($path, $outputName, FileReflector $reflector)
- {
+ private $projectRoot;
+ private $externalTypes;
+ private $componentId;
+ private $manifestPath;
+ private $release;
+ private $isComponent;
+
+ public function __construct(
+ $path,
+ $fileName,
+ FileReflector $reflector,
+ $projectRoot,
+ $componentId,
+ $manifestPath,
+ $release,
+ $isComponent = true
+ ) {
$this->path = $path;
- $this->outputName = $outputName;
+ $this->fileName = $fileName;
$this->reflector = $reflector;
$this->markdown = \Parsedown::instance();
+ $this->projectRoot = $projectRoot;
+ $this->externalTypes = json_decode(file_get_contents(__DIR__ .'/../../../../docs/external-classes.json'), true);
+ $this->componentId = $componentId;
+ $this->manifestPath = $manifestPath;
+ $this->release = $release;
+ $this->isComponent = $isComponent;
}
public function parse()
@@ -60,7 +86,7 @@ private function getReflector($fileReflector)
return $fileReflector->getTraits()[0];
}
- throw new \Exception('Could not get reflector for '. $this->outputName);
+ throw new \Exception('Could not get reflector for '. $this->fileName);
}
private function buildDocument($reflector)
@@ -120,8 +146,11 @@ private function buildDescription($docBlock, $content = null)
foreach ($parsedContents as &$part) {
if ($part instanceof Seetag) {
$reference = $part->getReference();
+
if (substr_compare($reference, 'Google\Cloud', 0, 12) === 0) {
$part = $this->buildLink($reference);
+ } elseif ($this->hasExternalType(trim(str_replace('@see', '', $part)))) {
+ $part = $this->buildExternalType(trim(str_replace('@see', '', $part)));
}
}
}
@@ -192,7 +221,7 @@ private function buildMethod($method)
'id' => $method->getName(),
'type' => $method->getName() === '__construct' ? 'constructor' : 'instance',
'name' => $method->getName(),
- 'source' => $this->outputName . '#L' . $method->getLineNumber(),
+ 'source' => $this->getSource() . '#L' . $method->getLineNumber(),
'description' => $this->buildDescription($docBlock, $split['description']),
'examples' => $this->buildExamples($split['examples']),
'resources' => $this->buildResources($resources),
@@ -225,7 +254,7 @@ private function buildMagicMethod($magicMethod)
'id' => $magicMethod->getMethodName(),
'type' => $magicMethod->getMethodName() === '__construct' ? 'constructor' : 'instance',
'name' => $magicMethod->getMethodName(),
- 'source' => $this->outputName,
+ 'source' => $this->getSource(),
'description' => $this->buildDescription($docBlock, $docText),
'examples' => $this->buildExamples($examples),
'resources' => $this->buildResources($resources),
@@ -415,8 +444,11 @@ private function buildReturns($returns)
$returnsArray = [];
foreach ($returns as $return) {
+ $context = $return->getDocBlock()->getContext();
+ $aliases = $context ? $context->getNamespaceAliases() : [];
+
$returnsArray[] = [
- 'types' => $this->handleTypes($return->getTypes()),
+ 'types' => $this->handleTypes($return->getTypes(), $aliases),
'description' => $this->buildDescription(null, $return->getDescription())
];
}
@@ -424,44 +456,136 @@ private function buildReturns($returns)
return $returnsArray;
}
- private function handleTypes($types)
+ private function handleTypes($types, array $aliases = [])
{
- foreach ($types as &$type) {
- // object is a PHPDoc keyword so it is not capable of detecting the context
- // https://github.com/phpDocumentor/ReflectionDocBlock/blob/2.0.4/src/phpDocumentor/Reflection/DocBlock/Type/Collection.php#L37
- if ($type === 'Object') {
- $type = '\Google\Cloud\Storage\Object';
- }
+ $res = [];
+ foreach ($types as $type) {
+ $matches = [];
- if (substr_compare($type, '\Google\Cloud', 0, 13) === 0) {
- $type = $this->buildLink($type);
- }
+ if (preg_match('/\\\\?(.*?)\<(.*?)\>/', $type, $matches)) {
+ $matches[1] = $this->resolveTypeAlias($matches[1], $aliases);
+ $matches[2] = $this->resolveTypeAlias($matches[2], $aliases);
- $matches = [];
- if (preg_match('/\\\\?Generator\<(.*?)\>/', $type, $matches)) {
- $typeLink = $matches[1];
+ $iteratorType = $matches[1];
if (strpos($matches[1], '\\') !== FALSE) {
- $typeLink = $this->buildLink($matches[1]);
+ $matches[1] = $this->buildLink($matches[1]);
}
- $type = sprintf(htmlentities('Generator<%s>'), $typeLink);
+ $typeLink = $matches[2];
+ if (strpos($matches[2], '\\') !== FALSE) {
+ $matches[2] = $this->buildLink($matches[2]);
+ }
+
+ $type = sprintf(htmlentities('%s<%s>'), $matches[1], $matches[2]);
+ } elseif (substr_compare($type, '\\Google\\Cloud', 0, 13) === 0) {
+ $type = $this->buildLink($type);
+ } elseif ($this->hasExternalType($type)) {
+ $type = $this->buildExternalType($type);
}
+
+ $res[] = $type;
+ }
+
+ return $res;
+ }
+
+ private function resolveTypeAlias($type, array $aliases)
+ {
+ $pieces = explode('\\', $type);
+ $basename = array_pop($pieces);
+ if (array_key_exists($basename, $aliases)) {
+ $type = $aliases[$basename];
+ }
+
+ return $type;
+ }
+
+ private function hasExternalType($type)
+ {
+ $type = trim($type, '\\');
+ $types = array_filter($this->externalTypes, function ($external) use ($type) {
+ return (strpos($type, $external['name']) !== false);
+ });
+
+ if (count($types) === 0) {
+ return false;
}
- return $types;
+ return true;
+ }
+
+ private function buildExternalType($type)
+ {
+ $type = trim($type, '\\');
+ $types = array_values(array_filter($this->externalTypes, function ($external) use ($type) {
+ return (strpos($type, $external['name']) !== false);
+ }));
+
+ $external = $types[0];
+
+ $href = sprintf($external['uri'], str_replace($external['name'], '', $type));
+ return sprintf(
+ '%s',
+ $href,
+ $type
+ );
}
private function buildLink($content)
{
- if ($content[0] === '\\') {
- $content = substr($content, 1);
+ $componentId = null;
+ if ($this->isComponent && substr_compare(trim($content, '\\'), 'Google\Cloud', 0, 12) === 0) {
+ try {
+ $matches = [];
+ preg_match('/[Generator\<]?(Google\\\Cloud\\\[\w\\\]{0,})[\>]?[\[\]]?/', $content, $matches);
+ $ref = new \ReflectionClass($matches[1]);
+ } catch (\ReflectionException $e) {
+ throw new \Exception(sprintf(
+ 'Reflection Exception: %s in %s. Given class was %s',
+ $e->getMessage(),
+ realpath($this->path),
+ $content
+ ));
+ }
+
+ $recurse = true;
+ $file = $ref->getFileName();
+
+ if (strpos($file, dirname(realpath($this->path))) !== false) {
+ $recurse = false;
+ }
+
+ do {
+ $composer = dirname($file) .'/composer.json';
+ if (file_exists($composer) && $component = $this->isComponent($composer)) {
+ $componentId = $component['id'];
+ if ($componentId === $this->componentId) {
+ $componentId = null;
+ }
+ $recurse = false;
+ } elseif (trim($file, '/') === trim($this->projectRoot, '/')) {
+ $recurse = false;
+ } else {
+ $file = dirname($file);
+ }
+ } while($recurse);
}
+ $content = trim($content, '\\');
+
$displayName = $content;
$content = substr($content, 13);
$parts = explode('::', $content);
$type = strtolower(str_replace('\\', '/', $parts[0]));
+ if ($componentId) {
+ $version = ($this->release)
+ ? $this->getComponentVersion($this->manifestPath, $componentId)
+ : 'master';
+
+ $type = $componentId .'/'. $version .'/'. $type;
+ }
+
$openTag = ' $examples
];
}
+
+ private function isComponent($composerPath)
+ {
+ if (isset(self::$composerFiles[$composerPath])) {
+ $contents = self::$composerFiles[$composerPath];
+ } else {
+ $contents = json_decode(file_get_contents($composerPath), true);
+ self::$composerFiles[$composerPath] = $contents;
+ }
+
+ if (isset($contents['extra']['component'])) {
+ return $contents['extra']['component'];
+ }
+
+ return false;
+ }
+
+ private function getSource()
+ {
+ return 'src' . explode('src', $this->path)[1];
+ }
}
diff --git a/dev/src/DocGenerator/Parser/MarkdownParser.php b/dev/src/DocGenerator/Parser/MarkdownParser.php
index 2599f80d4954..058dba85bbfe 100644
--- a/dev/src/DocGenerator/Parser/MarkdownParser.php
+++ b/dev/src/DocGenerator/Parser/MarkdownParser.php
@@ -49,7 +49,7 @@ public function parse()
$body = $doc->getElementsByTagName('body')->item(0);
return [
- 'id' => strtolower(substr($pathinfo['dirname'] .'/'. $pathinfo['filename'], 5)),
+ 'id' => strtolower(trim($pathinfo['dirname'] .'/'. $pathinfo['filename'], '/.')),
'type' => 'guide',
'title' => $heading->textContent,
'name' => $heading->textContent,
diff --git a/dev/src/DocGenerator/TableOfContents.php b/dev/src/DocGenerator/TableOfContents.php
new file mode 100644
index 000000000000..2192276f60f9
--- /dev/null
+++ b/dev/src/DocGenerator/TableOfContents.php
@@ -0,0 +1,84 @@
+template = $template;
+ $this->componentId = $componentId;
+ $this->componentVersion = $componentVersion;
+ $this->contentsPath = $contentsPath;
+ $this->outputPath = $outputPath;
+ $this->release = $release;
+ }
+
+ public function generate($pretty = false)
+ {
+ $toc = $this->getToc($this->componentId);
+
+ $tpl = $this->template;
+ $tpl['services'] = $this->services($toc);
+ $tpl['tagName'] = $this->release
+ ? $this->componentVersion
+ : 'master';
+
+ $writer = new Writer($tpl, $this->outputPath, $pretty);
+ $writer->write('toc.json');
+ }
+
+ private function services(array $toc)
+ {
+ $services = $toc['services'];
+
+ if (isset($toc['includes'])) {
+ foreach ($toc['includes'] as $include) {
+ $toc = $this->getToc($include);
+ $nested = $toc['services'];
+ $firstService = array_shift($nested);
+
+ $service = [
+ 'title' => $toc['title'],
+ 'type' => $firstService['type'],
+ 'nav' => $nested
+ ];
+
+ if (isset($toc['pattern'])) {
+ $service['patterns'] = [$toc['pattern']];
+ }
+
+ $services[] = $service;
+ }
+ }
+
+ return $services;
+ }
+
+ private function getToc($componentId)
+ {
+ return json_decode(file_get_contents($this->contentsPath .'/'. $componentId .'.json'), true);
+ }
+}
diff --git a/dev/src/DocGenerator/TypeGenerator.php b/dev/src/DocGenerator/TypeGenerator.php
index f05a39a6fcfc..8abfc8d4ac87 100644
--- a/dev/src/DocGenerator/TypeGenerator.php
+++ b/dev/src/DocGenerator/TypeGenerator.php
@@ -36,9 +36,9 @@ public function addType(array $type)
$this->types[] = $type;
}
- public function write()
+ public function write($pretty = false)
{
- $writer = new Writer(json_encode($this->types), $this->outputPath);
+ $writer = new Writer($this->types, $this->outputPath, $pretty);
$writer->write('types.json');
}
}
diff --git a/dev/src/DocGenerator/Writer.php b/dev/src/DocGenerator/Writer.php
index 4cafcb9200e3..f2c22e458b63 100644
--- a/dev/src/DocGenerator/Writer.php
+++ b/dev/src/DocGenerator/Writer.php
@@ -21,11 +21,13 @@ class Writer
{
private $content;
private $outputPath;
+ private $pretty;
- public function __construct($content, $outputPath)
+ public function __construct(array $content, $outputPath, $pretty = false)
{
$this->content = $content;
$this->outputPath = $outputPath;
+ $this->pretty = (bool) $pretty;
}
public function write($currentFile)
@@ -33,10 +35,14 @@ public function write($currentFile)
$path = $this->buildOutputPath($currentFile);
if (!is_dir(dirname($path))) {
- mkdir(dirname($path), 0777, true);
+ @mkdir(dirname($path), 0777, true);
}
- file_put_contents($path, $this->content);
+ $content = ($this->pretty)
+ ? json_encode($this->content, JSON_PRETTY_PRINT)
+ : json_encode($this->content);
+
+ file_put_contents($path, $content);
}
private function buildOutputPath($currentFile)
diff --git a/dev/src/Functions.php b/dev/src/Functions.php
new file mode 100644
index 000000000000..fea2e9cf8d88
--- /dev/null
+++ b/dev/src/Functions.php
@@ -0,0 +1,29 @@
+newInstanceArgs($args);
+}
diff --git a/dev/src/GetComponentsTrait.php b/dev/src/GetComponentsTrait.php
new file mode 100644
index 000000000000..3efa11290fe5
--- /dev/null
+++ b/dev/src/GetComponentsTrait.php
@@ -0,0 +1,116 @@
+ 1) {
+ $component['prefix'] = dirname('src' . $path[1]);
+ } else {
+ $component['prefix'] = '';
+ }
+
+ $components[] = $component;
+ }
+
+ return $components;
+ }
+
+ private function getComponentVersion($manifestPath, $componentId)
+ {
+ $manifest = $this->getComponentManifest($manifestPath, $componentId);
+ return $manifest['versions'][0];
+ }
+
+ private function getComponentManifest($manifestPath, $componentId)
+ {
+ $manifest = $this->getManifest($manifestPath);
+ $index = $this->getManifestComponentModuleIndex($manifestPath, $manifest, $componentId);
+
+ return $manifest['modules'][$index];
+ }
+
+ private function getManifestComponentModuleIndex($manifestPath, array $manifest, $componentId)
+ {
+ $modules = array_filter($this->getManifest($manifestPath)['modules'], function ($module) use ($componentId) {
+ return ($module['id'] === $componentId);
+ });
+
+ return array_keys($modules)[0];
+ }
+
+ private function getManifest($manifestPath)
+ {
+ if (self::$__manifest) {
+ $manifest = self::$__manifest;
+ } else {
+ $manifest = json_decode(file_get_contents($manifestPath), true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new RuntimeException('Could not decode manifest json');
+ }
+
+ self::$__manifest = $manifest;
+ }
+
+ return $manifest;
+ }
+
+ private function getComponentComposer($componentId)
+ {
+ $components = $this->getComponents($this->components, $this->defaultComponentComposer);
+
+ $components = array_values(array_filter($components, function ($component) use ($componentId) {
+ return ($component['id'] === $componentId);
+ }));
+
+ if (count($components) === 0) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Given component id %s is not a valid component.',
+ $componentId
+ ));
+ }
+
+ return $components[0];
+ }
+}
diff --git a/dev/src/Release/Command/Release.php b/dev/src/Release/Command/Release.php
index edc3bd1f21c3..37e0b62400c8 100644
--- a/dev/src/Release/Command/Release.php
+++ b/dev/src/Release/Command/Release.php
@@ -17,20 +17,35 @@
namespace Google\Cloud\Dev\Release\Command;
+use Google\Cloud\Dev\GetComponentsTrait;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use vierbergenlars\SemVer\version;
class Release extends Command
{
- const PATH_MANIFEST = 'docs/manifest.json';
- const PATH_SERVICE_BUILDER = 'src/ServiceBuilder.php';
+ use GetComponentsTrait;
+
+ const COMPONENT_BASE = '%s/../src';
+ const DEFAULT_COMPONENT = 'google-cloud';
+ const DEFAULT_COMPONENT_COMPOSER = '%s/../composer.json';
+ const PATH_MANIFEST = '%s/../docs/manifest.json';
+ const PATH_SERVICE_BUILDER = '%s/../src/ServiceBuilder.php';
private $cliBasePath;
+ private $defaultClient;
+
+ private $manifest;
+
+ private $defaultComponentComposer;
+
+ private $components;
+
private $allowedReleaseTypes = [
'major', 'minor', 'patch'
];
@@ -39,6 +54,11 @@ public function __construct($cliBasePath)
{
$this->cliBasePath = $cliBasePath;
+ $this->defaultClient = sprintf(self::PATH_SERVICE_BUILDER, $cliBasePath);
+ $this->manifest = sprintf(self::PATH_MANIFEST, $cliBasePath);
+ $this->defaultComponentComposer = sprintf(self::DEFAULT_COMPONENT_COMPOSER, $cliBasePath);
+ $this->components = sprintf(self::COMPONENT_BASE, $cliBasePath);
+
parent::__construct();
}
@@ -46,23 +66,31 @@ protected function configure()
{
$this->setName('release')
->setDescription('Prepares a new release')
- ->addArgument('version', InputArgument::REQUIRED, 'The new version number');
+ ->addArgument('version', InputArgument::REQUIRED, 'The new version number.')
+ ->addOption(
+ 'component',
+ 'c',
+ InputOption::VALUE_REQUIRED,
+ 'The component for which the version should be updated.',
+ self::DEFAULT_COMPONENT
+ );
}
protected function execute(InputInterface $input, OutputInterface $output)
{
+ $component = $this->getComponentComposer($input->getOption('component'));
+
$version = $input->getArgument('version');
+
+ // If the version is one of "major", "minor" or "patch", determine the
+ // correct incrementation.
if (in_array(strtolower($version), $this->allowedReleaseTypes)) {
- $version = $this->getNextVersionName($version);
+ $version = $this->getNextVersionName($version, $component);
}
try {
$validatedVersion = new version($version);
} catch (\Exception $e) {
- $validatedVersion = null;
- }
-
- if (is_null($validatedVersion)) {
throw new RuntimeException(sprintf(
'Given version %s is not a valid version name',
$version
@@ -72,18 +100,33 @@ protected function execute(InputInterface $input, OutputInterface $output)
$version = (string) $validatedVersion;
$output->writeln(sprintf(
- 'Adding version %s to Documentation Manifest.',
- $version
+ 'Adding version %s to Documentation Manifest for component %s.',
+ $version,
+ $component['id']
));
- $this->addToManifest($version);
+ $this->addToComponentManifest($version, $component);
$output->writeln(sprintf(
- 'Setting ServiceBuilder version constant to %s.',
+ 'Setting component version constant to %s.',
$version
));
- $this->updateServiceBuilder($version);
+ $this->updateComponentVersionConstant($version, $component);
+ $output->writeln(sprintf(
+ 'File %s VERSION constant updated to %s',
+ $component['entry'],
+ $version
+ ));
+
+ if ($component['id'] !== 'google-cloud') {
+ $this->updateComponentVersionFile($version, $component);
+ $output->writeln(sprintf(
+ 'Component %s VERSION file updated to %s',
+ $component['id'],
+ $version
+ ));
+ }
$output->writeln(sprintf(
'Release %s generated!',
@@ -91,64 +134,72 @@ protected function execute(InputInterface $input, OutputInterface $output)
));
}
- private function getNextVersionName($type)
+ private function getNextVersionName($type, array $component)
{
- $manifest = $this->getManifest();
- $lastRelease = new version($manifest['versions'][0]);
+ $manifest = $this->getComponentManifest($this->manifest, $component['id']);
+
+ if ($manifest['versions'][0] === 'master') {
+ $lastRelease = new version('0.0.0');
+ } else {
+ $lastRelease = new version($manifest['versions'][0]);
+ }
return $lastRelease->inc($type);
}
- private function addToManifest($version)
+ private function addToComponentManifest($version, array $component)
{
- $manifest = $this->getManifest();
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Could not decode manifest json');
- }
+ $manifest = $this->getManifest($this->manifest);
+ $index = $this->getManifestComponentModuleIndex($this->manifest, $manifest, $component['id']);
- array_unshift($manifest['versions'], 'v'. $version);
+ array_unshift($manifest['modules'][$index]['versions'], 'v'. $version);
$content = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ."\n";
- $result = file_put_contents($this->getManifestPath(), $content);
+ $result = file_put_contents($this->manifest, $content);
if (!$result) {
throw new RuntimeException('File write failed');
}
}
- private function updateServiceBuilder($version)
+ private function updateComponentVersionConstant($version, array $component)
{
- $path = $this->cliBasePath .'/../'. self::PATH_SERVICE_BUILDER;
+ if (is_null($component['entry'])) {
+ return false;
+ }
+
+ $path = $this->cliBasePath .'/../'. $component['path'] .'/'. $component['entry'];
if (!file_exists($path)) {
- throw new RuntimeException('ServiceBuilder not found at '. $path);
+ throw new \RuntimeException(sprintf(
+ 'Component entry file %s does not exist',
+ $path
+ ));
}
- $sb = file_get_contents($path);
+ $entry = file_get_contents($path);
$replacement = sprintf("const VERSION = '%s';", $version);
- $sb = preg_replace("/const VERSION = '[0-9.]{0,}'\;/", $replacement, $sb);
+ $entry = preg_replace("/const VERSION = [\'\\\"]([0-9.]{0,}|master)[\'\\\"]\;/", $replacement, $entry);
- $result = file_put_contents($path, $sb);
+ $result = file_put_contents($path, $entry);
if (!$result) {
throw new RuntimeException('File write failed');
}
+
+ return true;
}
- private function getManifest()
+ private function updateComponentVersionFile($version, array $component)
{
- $path = $this->getManifestPath();
- if (!file_exists($path)) {
- throw new RuntimeException('Manifest file not found at '. $path);
- }
+ $path = $this->cliBasePath .'/../'. $component['path'] .'/VERSION';
+ $result = file_put_contents($path, $version);
- return json_decode(file_get_contents($path), true);
- }
+ if (!$result) {
+ throw new RuntimeException('File write failed');
+ }
- private function getManifestPath()
- {
- return $this->cliBasePath .'/../'. self::PATH_MANIFEST;
+ return true;
}
}
diff --git a/dev/src/Snippet/Parser/Parser.php b/dev/src/Snippet/Parser/Parser.php
index c2ec03f75674..06007b9817d1 100644
--- a/dev/src/Snippet/Parser/Parser.php
+++ b/dev/src/Snippet/Parser/Parser.php
@@ -176,6 +176,10 @@ public function examplesFromMethod($class, $method)
$method = new ReflectionMethod($class, $method);
}
+ if (!$method->isPublic()) {
+ return [];
+ }
+
$doc = new DocBlock($method);
$parent = $method->getDeclaringClass();
diff --git a/dev/src/Snippet/Parser/Snippet.php b/dev/src/Snippet/Parser/Snippet.php
index 74316ecaf25d..d1ace59affdf 100644
--- a/dev/src/Snippet/Parser/Snippet.php
+++ b/dev/src/Snippet/Parser/Snippet.php
@@ -152,9 +152,14 @@ public function invoke($returnVar = null)
$cb = function($return) use ($content) {
extract($this->locals);
- ob_start();
- $res = eval($content ."\n\n". $return);
- $out = ob_get_clean();
+ try {
+ ob_start();
+ $res = eval($content ."\n\n". $return);
+ $out = ob_get_clean();
+ } catch (\Exception $e) {
+ ob_end_clean();
+ throw $e;
+ }
return new InvokeResult($res, $out);
};
diff --git a/dev/src/Snippet/SnippetTestCase.php b/dev/src/Snippet/SnippetTestCase.php
index 98349768a57d..1bbf1e9ce293 100644
--- a/dev/src/Snippet/SnippetTestCase.php
+++ b/dev/src/Snippet/SnippetTestCase.php
@@ -26,18 +26,13 @@
*/
class SnippetTestCase extends \PHPUnit_Framework_TestCase
{
- const HOOK_BEFORE = 1000;
- const HOOK_AFTER = 1001;
+ private static $coverage;
+ private static $parser;
- private $coverage;
- private $parser;
-
- public function __construct()
+ public static function setUpBeforeClass()
{
- parent::__construct();
-
- $this->coverage = Container::$coverage;
- $this->parser = Container::$parser;
+ self::$coverage = Container::$coverage;
+ self::$parser = Container::$parser;
}
/**
@@ -49,14 +44,14 @@ public function __construct()
*/
public function snippetFromClass($class, $indexOrName = 0)
{
- $identifier = $this->parser->createIdentifier($class, $indexOrName);
+ $identifier = self::$parser->createIdentifier($class, $indexOrName);
- $snippet = $this->coverage->cache($identifier);
+ $snippet = self::$coverage->cache($identifier);
if (!$snippet) {
- $snippet = $this->parser->classExample($class, $indexOrName);
+ $snippet = self::$parser->classExample($class, $indexOrName);
}
- $this->coverage->cover($snippet->identifier());
+ self::$coverage->cover($snippet->identifier());
return $snippet;
}
@@ -73,14 +68,14 @@ public function snippetFromClass($class, $indexOrName = 0)
public function snippetFromMagicMethod($class, $method, $indexOrName = 0)
{
$name = $class .'::'. $method;
- $identifier = $this->parser->createIdentifier($name, $indexOrName);
+ $identifier = self::$parser->createIdentifier($name, $indexOrName);
- $snippet = $this->coverage->cache($identifier);
+ $snippet = self::$coverage->cache($identifier);
if (!$snippet) {
throw new \Exception('Magic Method '. $name .' was not found');
}
- $this->coverage->cover($identifier);
+ self::$coverage->cover($identifier);
return $snippet;
}
@@ -96,14 +91,14 @@ public function snippetFromMagicMethod($class, $method, $indexOrName = 0)
public function snippetFromMethod($class, $method, $indexOrName = 0)
{
$name = $class .'::'. $method;
- $identifier = $this->parser->createIdentifier($name, $indexOrName);
+ $identifier = self::$parser->createIdentifier($name, $indexOrName);
- $snippet = $this->coverage->cache($identifier);
+ $snippet = self::$coverage->cache($identifier);
if (!$snippet) {
- $snippet = $this->parser->methodExample($class, $method, $indexOrName);
+ $snippet = self::$parser->methodExample($class, $method, $indexOrName);
}
- $this->coverage->cover($identifier);
+ self::$coverage->cover($identifier);
return $snippet;
}
diff --git a/dev/src/Split/Command/Split.php b/dev/src/Split/Command/Split.php
new file mode 100644
index 000000000000..6959e6985ac7
--- /dev/null
+++ b/dev/src/Split/Command/Split.php
@@ -0,0 +1,172 @@
+cliBasePath = $cliBasePath;
+ $this->splitShell = sprintf(self::SPLIT_SHELL, $cliBasePath);
+ $this->components = sprintf(self::COMPONENT_BASE, $cliBasePath);
+ $this->manifest = sprintf(self::PATH_MANIFEST, $cliBasePath);
+
+ $this->http = new Client;
+ $this->token = getenv(self::TOKEN_ENV);
+
+ parent::__construct();
+ }
+
+ protected function configure()
+ {
+ $this->setName('split')
+ ->setDescription('Split subtree and push to various remotes.');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ if (!getenv(self::TAG_ENV)) {
+ $output->writeln('This command should only be run inside a CI post-release process');
+ return;
+ }
+
+ $components = $this->getComponents($this->components);
+
+ $tag = getenv(self::TAG_ENV);
+
+ $parentTagSource = sprintf(self::PARENT_TAG_NAME, $tag);
+
+ foreach ($components as $component) {
+ $output->writeln('');
+ $output->writeln(sprintf('Starting on component %s', $component['id']));
+ $output->writeln('------------');
+ shell_exec(sprintf(
+ '%s %s %s',
+ $this->splitShell,
+ $component['prefix'],
+ $component['target']
+ ));
+
+ $target = $component['target'];
+ $matches = [];
+ preg_match(self::TARGET_REGEX, $target, $matches);
+
+ $org = $matches[1];
+ $repo = $matches[2];
+
+ $version = $this->getComponentVersion($this->manifest, $component['id']);
+ try {
+ new version($version);
+ } catch (SemVerException $e) {
+ $output->writeln(sprintf(
+ 'Component %s version %s is invalid.',
+ $component['id'],
+ $version
+ ));
+ continue;
+ }
+
+ if ($this->doesTagExist($version, $org, $repo)) {
+ $output->writeln(sprintf(
+ 'Component %s already tagged at version %s',
+ $component['id'],
+ $version
+ ));
+ continue;
+ }
+
+ $name = $component['displayName'] .' '. $version;
+ $notes = sprintf(
+ 'For release notes, please see the [associated Google Cloud PHP release](%s).',
+ $parentTagSource
+ );
+ $this->createRelease($version, $org, $repo, $name, $notes);
+
+ $output->writeln(sprintf(
+ 'Release %s created for component %s',
+ $version,
+ $component['id']
+ ));
+ }
+ }
+
+ private function doesTagExist($tagName, $org, $repo)
+ {
+ $res = $this->http->get(sprintf(
+ self::GITHUB_RELEASES_ENDPOINT,
+ $org, $repo, $tagName
+ ), [
+ 'http_errors' => false,
+ 'auth' => [null, $this->token]
+ ]);
+
+ return ($res->getStatusCode() === 200);
+ }
+
+ private function createRelease($tagName, $org, $repo, $name, $notes)
+ {
+ $requestBody = [
+ 'tag_name' => $tagName,
+ 'name' => $name,
+ 'body' => $notes
+ ];
+
+ $res = $this->http->post(sprintf(
+ self::GITHUB_RELEASE_CREATE_ENDPOINT,
+ $org, $repo
+ ), [
+ 'http_errors' => false,
+ 'json' => $requestBody,
+ 'auth' => [null, $this->token]
+ ]);
+ }
+}
diff --git a/dev/src/StubTrait.php b/dev/src/StubTrait.php
new file mode 100644
index 000000000000..7a710ed8bb72
--- /dev/null
+++ b/dev/src/StubTrait.php
@@ -0,0 +1,55 @@
+___getPropertyReflector($prop);
+
+ $property->setAccessible(true);
+ return $property->getValue($this);
+ }
+
+ public function ___setProperty($prop, $value)
+ {
+ if (!in_array($prop, json_decode($this->___props))) {
+ throw new \BadMethodCallException(sprintf('Property %s cannot be overloaded', $prop));
+ }
+
+ $property = $this->___getPropertyReflector($prop);
+
+ $property->setAccessible(true);
+ $property->setValue($this, $value);
+ }
+
+ private function ___getPropertyReflector($property)
+ {
+ $trait = new \ReflectionClass($this);
+ $ref = $trait->getParentClass();
+
+ try {
+ $property = $ref->getProperty($property);
+ } catch (\ReflectionException $e) {
+ throw new \BadMethodCallException($e->getMessage());
+ }
+
+ return $property;
+ }
+}
diff --git a/docs/contents/cloud-bigquery.json b/docs/contents/cloud-bigquery.json
new file mode 100644
index 000000000000..5fb32b3044f9
--- /dev/null
+++ b/docs/contents/cloud-bigquery.json
@@ -0,0 +1,35 @@
+{
+ "title": "BigQuery",
+ "pattern": "bigquery\/\\w{1,}",
+ "services": [{
+ "title": "BigQueryClient",
+ "type": "bigquery/bigqueryclient"
+ }, {
+ "title": "Bytes",
+ "type": "bigquery/bytes"
+ }, {
+ "title": "Dataset",
+ "type": "bigquery/dataset"
+ }, {
+ "title": "Date",
+ "type": "bigquery/date"
+ }, {
+ "title": "InsertResponse",
+ "type": "bigquery/insertresponse"
+ }, {
+ "title": "Job",
+ "type": "bigquery/job"
+ }, {
+ "title": "QueryResults",
+ "type": "bigquery/queryresults"
+ }, {
+ "title": "Table",
+ "type": "bigquery/table"
+ }, {
+ "title": "Time",
+ "type": "bigquery/time"
+ }, {
+ "title": "Timestamp",
+ "type": "bigquery/timestamp"
+ }]
+}
diff --git a/docs/contents/cloud-core.json b/docs/contents/cloud-core.json
new file mode 100644
index 000000000000..3b0b80bc4949
--- /dev/null
+++ b/docs/contents/cloud-core.json
@@ -0,0 +1,37 @@
+{
+ "title": "Core",
+ "pattern": "core\/\\w{1,}",
+ "services": [{
+ "title": "Overview",
+ "type": "core/readme"
+ }, {
+ "title": "IAM",
+ "type": "core/iam/iam",
+ "patterns": [
+ "core/iam/\\w{1,}"
+ ],
+ "nav": [{
+ "title": "PolicyBuilder",
+ "type": "core/iam/policybuilder"
+ }]
+ }, {
+ "title": "Upload",
+ "type": "core/upload/abstractuploader",
+ "patterns": [
+ "core/upload/\\w{1,}"
+ ],
+ "nav": [{
+ "title": "MultipartUploader",
+ "type": "core/upload/multipartuploader"
+ }, {
+ "title": "ResumableUploader",
+ "type": "core/upload/resumableuploader"
+ }, {
+ "title": "StreamableUploader",
+ "type": "core/upload/streamableuploader"
+ }]
+ }, {
+ "title": "Int64",
+ "type": "core/int64"
+ }]
+}
diff --git a/docs/contents/cloud-datastore.json b/docs/contents/cloud-datastore.json
new file mode 100644
index 000000000000..44e37171d7bd
--- /dev/null
+++ b/docs/contents/cloud-datastore.json
@@ -0,0 +1,33 @@
+{
+ "title": "Datastore",
+ "pattern": "datastore\/\\w{1,}",
+ "services": [{
+ "title": "DatastoreClient",
+ "type": "datastore/datastoreclient"
+ }, {
+ "title": "Transaction",
+ "type": "datastore/transaction"
+ }, {
+ "title": "Entity",
+ "type": "datastore/entity"
+ }, {
+ "title": "Key",
+ "type": "datastore/key"
+ }, {
+ "title": "GeoPoint",
+ "type": "datastore/geopoint"
+ }, {
+ "title": "Blob",
+ "type": "datastore/blob"
+ }, {
+ "title": "Queries",
+ "type": "datastore/queryinterface",
+ "nav": [{
+ "title": "GQL Query",
+ "type": "datastore/query/gqlquery"
+ }, {
+ "title": "Query",
+ "type": "datastore/query/query"
+ }]
+ }]
+}
diff --git a/docs/contents/cloud-error-reporting.json b/docs/contents/cloud-error-reporting.json
new file mode 100644
index 000000000000..b1801f20f9fc
--- /dev/null
+++ b/docs/contents/cloud-error-reporting.json
@@ -0,0 +1,24 @@
+{
+ "title": "Error Reporting",
+ "pattern": "errorreporting\/\\w{1,}",
+ "services": [{
+ "title": "Overview",
+ "type": "errorreporting/readme"
+ }, {
+ "title": "v1beta1",
+ "type": "errorreporting/v1beta1/readme",
+ "patterns": [
+ "errorreporting/v1beta1/\\w{1,}"
+ ],
+ "nav": [{
+ "title": "ErrorGroupServiceClient",
+ "type": "errorreporting/v1beta1/errorgroupserviceclient"
+ }, {
+ "title": "ErrorStatsServiceClient",
+ "type": "errorreporting/v1beta1/errorstatsserviceclient"
+ }, {
+ "title": "ReportErrorsServiceClient",
+ "type": "errorreporting/v1beta1/reporterrorsserviceclient"
+ }]
+ }]
+}
diff --git a/docs/contents/cloud-logging.json b/docs/contents/cloud-logging.json
new file mode 100644
index 000000000000..a8b8d02422ac
--- /dev/null
+++ b/docs/contents/cloud-logging.json
@@ -0,0 +1,39 @@
+{
+ "title": "Logging",
+ "pattern": "logging\/\\w{1,}",
+ "services": [{
+ "title": "LoggingClient",
+ "type": "logging/loggingclient"
+ }, {
+ "title": "Entry",
+ "type": "logging/entry"
+ }, {
+ "title": "Logger",
+ "type": "logging/logger"
+ }, {
+ "title": "Metric",
+ "type": "logging/metric"
+ },{
+ "title": "PsrLogger",
+ "type": "logging/psrlogger"
+ },{
+ "title": "Sink",
+ "type": "logging/sink"
+ }, {
+ "title": "v2",
+ "type": "logging/v2/readme",
+ "patterns": [
+ "logging/v2/\\w{1,}"
+ ],
+ "nav": [{
+ "title": "ConfigServiceV2Client",
+ "type": "logging/v2/configservicev2client"
+ }, {
+ "title": "LoggingServiceV2Client",
+ "type": "logging/v2/loggingservicev2client"
+ }, {
+ "title": "MetricsServiceV2Client",
+ "type": "logging/v2/metricsservicev2client"
+ }]
+ }]
+}
diff --git a/docs/contents/cloud-monitoring.json b/docs/contents/cloud-monitoring.json
new file mode 100644
index 000000000000..d6ff59d0ded5
--- /dev/null
+++ b/docs/contents/cloud-monitoring.json
@@ -0,0 +1,21 @@
+{
+ "title": "Monitoring",
+ "pattern": "monitoring\/\\w{1,}",
+ "services": [{
+ "title": "Overview",
+ "type": "monitoring/readme"
+ }, {
+ "title": "v3",
+ "type": "monitoring/v3/readme",
+ "patterns": [
+ "monitoring/v3/\\w{1,}"
+ ],
+ "nav": [{
+ "title": "GroupServiceClient",
+ "type": "monitoring/v3/groupserviceclient"
+ }, {
+ "title": "MetricServiceClient",
+ "type": "monitoring/v3/metricserviceclient"
+ }]
+ }]
+}
diff --git a/docs/contents/cloud-natural-language.json b/docs/contents/cloud-natural-language.json
new file mode 100644
index 000000000000..6552ba78fcf3
--- /dev/null
+++ b/docs/contents/cloud-natural-language.json
@@ -0,0 +1,11 @@
+{
+ "title": "Natural Language",
+ "pattern": "naturallanguage\/\\w{1,}",
+ "services": [{
+ "title": "NaturalLanguageClient",
+ "type": "naturallanguage/naturallanguageclient"
+ }, {
+ "title": "Annotation",
+ "type": "naturallanguage/annotation"
+ }]
+}
diff --git a/docs/contents/cloud-pubsub.json b/docs/contents/cloud-pubsub.json
new file mode 100644
index 000000000000..795fe56f064a
--- /dev/null
+++ b/docs/contents/cloud-pubsub.json
@@ -0,0 +1,30 @@
+{
+ "title": "Pub/Sub",
+ "pattern": "pubsub\/\\w{1,}",
+ "services": [{
+ "title": "PubSubClient",
+ "type": "pubsub/pubsubclient"
+ }, {
+ "title": "Message",
+ "type": "pubsub/message"
+ }, {
+ "title": "Subscription",
+ "type": "pubsub/subscription"
+ }, {
+ "title": "Topic",
+ "type": "pubsub/topic"
+ }, {
+ "title": "v1",
+ "type": "pubsub/v1/readme",
+ "patterns": [
+ "pubsub/v1/\\w{1,}"
+ ],
+ "nav": [{
+ "title": "PublisherClient",
+ "type": "pubsub/v1/publisherclient"
+ }, {
+ "title": "SubscriberClient",
+ "type": "pubsub/v1/subscriberclient"
+ }]
+ }]
+}
diff --git a/docs/contents/cloud-speech.json b/docs/contents/cloud-speech.json
new file mode 100644
index 000000000000..976f9acda989
--- /dev/null
+++ b/docs/contents/cloud-speech.json
@@ -0,0 +1,21 @@
+{
+ "title": "Speech",
+ "pattern": "speech\/\\w{1,}",
+ "services": [{
+ "title": "SpeechClient",
+ "type": "speech/speechclient"
+ }, {
+ "title": "Operation",
+ "type": "speech/operation"
+ }, {
+ "title": "v1beta1",
+ "type": "speech/v1beta1/readme",
+ "patterns": [
+ "speech/v1beta1/\\w{1,}"
+ ],
+ "nav": [{
+ "title": "SpeechClient",
+ "type": "speech/v1beta1/speechclient"
+ }]
+ }]
+}
diff --git a/docs/contents/cloud-storage.json b/docs/contents/cloud-storage.json
new file mode 100644
index 000000000000..2105294f8fab
--- /dev/null
+++ b/docs/contents/cloud-storage.json
@@ -0,0 +1,17 @@
+{
+ "title": "Storage",
+ "pattern": "storage\/\\w{1,}",
+ "services": [{
+ "title": "StorageClient",
+ "type": "storage/storageclient"
+ }, {
+ "title": "ACL",
+ "type": "storage/acl"
+ }, {
+ "title": "Bucket",
+ "type": "storage/bucket"
+ }, {
+ "title": "StorageObject",
+ "type": "storage/storageobject"
+ }]
+}
diff --git a/docs/contents/cloud-translate.json b/docs/contents/cloud-translate.json
new file mode 100644
index 000000000000..4ee6a8b7c3eb
--- /dev/null
+++ b/docs/contents/cloud-translate.json
@@ -0,0 +1,8 @@
+{
+ "title": "Translation",
+ "pattern": "translate\/\\w{1,}",
+ "services": [{
+ "title": "TranslateClient",
+ "type": "translate/translateclient"
+ }]
+}
diff --git a/docs/contents/cloud-vision.json b/docs/contents/cloud-vision.json
new file mode 100644
index 000000000000..356f4d804828
--- /dev/null
+++ b/docs/contents/cloud-vision.json
@@ -0,0 +1,38 @@
+{
+ "title": "Vision",
+ "pattern": "vision\/\\w{1,}",
+ "services": [{
+ "title": "VisionClient",
+ "type": "vision/visionclient"
+ }, {
+ "title": "Image",
+ "type": "vision/image"
+ }, {
+ "title": "Annotation",
+ "type": "vision/annotation",
+ "nav": [
+ {
+ "title": "CropHint",
+ "type": "vision/annotation/crophint"
+ }, {
+ "title": "Document",
+ "type": "vision/annotation/document"
+ }, {
+ "title": "Entity",
+ "type": "vision/annotation/entity"
+ }, {
+ "title": "Face",
+ "type": "vision/annotation/face"
+ }, {
+ "title": "ImageProperties",
+ "type": "vision/annotation/imageproperties"
+ }, {
+ "title": "SafeSearch",
+ "type": "vision/annotation/safesearch"
+ }, {
+ "title": "Web",
+ "type": "vision/annotation/web"
+ }
+ ]
+ }]
+}
diff --git a/docs/contents/google-cloud.json b/docs/contents/google-cloud.json
new file mode 100644
index 000000000000..0da62d46cb8d
--- /dev/null
+++ b/docs/contents/google-cloud.json
@@ -0,0 +1,21 @@
+{
+ "title": "Google Cloud PHP",
+ "services": [{
+ "title": "ServiceBuilder",
+ "type": "servicebuilder"
+ }],
+ "includes": [
+ "cloud-bigquery",
+ "cloud-datastore",
+ "cloud-error-reporting",
+ "cloud-logging",
+ "cloud-monitoring",
+ "cloud-natural-language",
+ "cloud-pubsub",
+ "cloud-speech",
+ "cloud-storage",
+ "cloud-translate",
+ "cloud-vision",
+ "cloud-core"
+ ]
+}
diff --git a/docs/external-classes.json b/docs/external-classes.json
new file mode 100644
index 000000000000..580897786b30
--- /dev/null
+++ b/docs/external-classes.json
@@ -0,0 +1,83 @@
+[{
+ "name": "Google\\Auth\\",
+ "uri": "https://github.com/google/google-auth-library-php/blob/master/src/%s.php"
+}, {
+ "name": "Google\\GAX\\",
+ "uri": "https://github.com/googleapis/gax-php/tree/master/src/%s.php"
+}, {
+ "name": "google\\iam\\v1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/iam/v1"
+}, {
+ "name": "google\\devtools\\clouderrorreporting\\v1beta1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/errorreporting/v1beta1"
+}, {
+ "name": "google\\cloud\\language\\v1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/language/v1"
+}, {
+ "name": "google\\logging\\v2\\",
+ "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/logging/v2"
+}, {
+ "name": "google\\monitoring\\v3\\",
+ "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/monitoring/v3"
+}, {
+ "name": "google\\pubsub\\v1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/pubsub/v1/pubsub.php"
+}, {
+ "name": "google\\spanner\\admin\\database\\v1\\",
+ "name": "https://github.com/googleapis/proto-client-php/blob/master/src/spanner/admin/database/v1/spanner_database_admin.php"
+}, {
+ "name": "google\\spanner\\admin\\instance\\v1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/spanner/admin/instance/v1/spanner_instance_admin.php"
+}, {
+ "name": "google\\spanner\\v1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/spanner/v1"
+}, {
+ "name": "google\\cloud\\speech\\v1beta1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/speech/v1beta1/cloud_speech.php"
+}, {
+ "name": "google\\devtools\\cloudtrace\\v1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/trace/v1/trace.php"
+}, {
+ "name": "google\\cloud\\vision\\v1\\",
+ "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/vision/v1"
+}, {
+ "name": "google\\longrunning\\",
+ "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/operations.php"
+}, {
+ "name": "MonitoredResource",
+ "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/monitored_resource.php"
+}, {
+ "name": "ServiceContextFilter",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/errorreporting/v1beta1/error_stats_service.php#L1985"
+}, {
+ "name": "QueryTimeRange",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/errorreporting/v1beta1/error_stats_service.php#L1915"
+}, {
+ "name": "Duration",
+ "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/well-known-types/duration.php"
+}, {
+ "name": "TimedCountAlignment",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/errorreporting/v1beta1/error_stats_service.php#L8"
+}, {
+ "name": "Timestamp",
+ "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/well-known-types/timestamp.php"
+}, {
+ "name": "ErrorGroupOrder",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/errorreporting/v1beta1/error_stats_service.php#L16"
+}, {
+ "name": "TimeInterval",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/monitoring/v3/common.php#L270"
+}, {
+ "name": "google\\api\\MetricDescriptor",
+ "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/metric.php#L29"
+}, {
+ "name": "google\\api\\MonitoredResourceDescriptor",
+ "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/monitored_resource.php#L8"
+}, {
+ "name": "Aggregation",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/monitoring/v3/common.php#L429"
+}, {
+ "name": "PushConfig",
+ "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/pubsub/v1/pubsub.php#L1827"
+}]
+
diff --git a/docs/home.html b/docs/home.html
index 46eb81f7d874..82f9296f1687 100644
--- a/docs/home.html
+++ b/docs/home.html
@@ -22,7 +22,7 @@
-
-
+
Read the Docs
diff --git a/docs/manifest.json b/docs/manifest.json
index 09802d20c00b..09fdb29a0719 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -3,33 +3,152 @@
"friendlyLang": "PHP",
"libraryTitle": "Google Cloud Client Library",
"moduleName": "google-cloud-php",
- "defaultService": "servicebuilder",
- "matchPartialServiceId": true,
"markdown": "php",
- "versions": [
- "v0.20.2",
- "v0.20.1",
- "v0.20.0",
- "v0.13.2",
- "v0.13.1",
- "v0.13.0",
- "v0.12.0",
- "v0.11.1",
- "v0.11.0",
- "v0.10.2",
- "v0.10.1",
- "v0.10.0",
- "v0.9.0",
- "v0.8.0",
- "v0.7.1",
- "v0.7.0",
- "v0.6.0",
- "v0.5.1",
- "v0.5.0",
- "v0.4.1",
- "v0.4.0",
- "v0.3.0",
- "master"
+ "defaultModule": "google-cloud",
+ "modules": [
+ {
+ "id": "google-cloud",
+ "name": "google/cloud",
+ "defaultService": "servicebuilder",
+ "versions": [
+ "v0.24.0",
+ "v0.23.0",
+ "v0.22.0",
+ "v0.21.1",
+ "v0.21.0",
+ "v0.20.2",
+ "v0.20.1",
+ "v0.20.0",
+ "v0.13.2",
+ "v0.13.1",
+ "v0.13.0",
+ "v0.12.0",
+ "v0.11.1",
+ "v0.11.0",
+ "v0.10.2",
+ "v0.10.1",
+ "v0.10.0",
+ "v0.9.0",
+ "v0.8.0",
+ "v0.7.1",
+ "v0.7.0",
+ "v0.6.0",
+ "v0.5.1",
+ "v0.5.0",
+ "v0.4.1",
+ "v0.4.0",
+ "v0.3.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-bigquery",
+ "name": "google/cloud-bigquery",
+ "defaultService": "bigquery/bigqueryclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-core",
+ "name": "google/cloud-core",
+ "defaultService": "core/readme",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-datastore",
+ "name": "google/cloud-datastore",
+ "defaultService": "datastore/datastoreclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-error-reporting",
+ "name": "google/cloud-error-reporting",
+ "defaultService": "errorreporting/readme",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-logging",
+ "name": "google/cloud-logging",
+ "defaultService": "logging/loggingclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-monitoring",
+ "name": "google/cloud-monitoring",
+ "defaultService": "monitoring/readme",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-natural-language",
+ "name": "google/cloud-natural-language",
+ "defaultService": "naturallanguage/naturallanguageclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-pubsub",
+ "name": "google/cloud-pubsub",
+ "defaultService": "pubsub/pubsubclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-speech",
+ "name": "google/cloud-speech",
+ "defaultService": "speech/speechclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-storage",
+ "name": "google/cloud-storage",
+ "defaultService": "storage/storageclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-translate",
+ "name": "google/cloud-translate",
+ "defaultService": "translate/translateclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ },
+ {
+ "id": "cloud-vision",
+ "name": "google/cloud-vision",
+ "defaultService": "vision/visionclient",
+ "versions": [
+ "v0.1.0",
+ "master"
+ ]
+ }
],
"content": "json",
"home": "home.html",
diff --git a/docs/toc.json b/docs/toc.json
index 447db977acce..853743a07998 100644
--- a/docs/toc.json
+++ b/docs/toc.json
@@ -30,149 +30,5 @@
"edit": "https://github.com/GoogleCloudPlatform/gcloud-common/edit/master/contributing/readme.md",
"contents": "https://raw.githubusercontent.com/GoogleCloudPlatform/gcloud-common/master/contributing/readme.md"
}],
- "services": [
- {
- "title": "ServiceBuilder",
- "type": "servicebuilder"
- },
- {
- "title": "BigQuery",
- "type": "bigquery/bigqueryclient",
- "nav": [{
- "title": "Bytes",
- "type": "bigquery/bytes"
- }, {
- "title": "Dataset",
- "type": "bigquery/dataset"
- }, {
- "title": "Date",
- "type": "bigquery/date"
- }, {
- "title": "InsertResponse",
- "type": "bigquery/insertresponse"
- }, {
- "title": "Job",
- "type": "bigquery/job"
- }, {
- "title": "QueryResults",
- "type": "bigquery/queryresults"
- }, {
- "title": "Table",
- "type": "bigquery/table"
- }, {
- "title": "Time",
- "type": "bigquery/time"
- }, {
- "title": "Timestamp",
- "type": "bigquery/timestamp"
- }]
- },
- {
- "title": "Datastore",
- "type": "datastore/datastoreclient",
- "nav": [{
- "title": "Transaction",
- "type": "datastore/transaction"
- }, {
- "title": "Entity",
- "type": "datastore/entity"
- }, {
- "title": "Key",
- "type": "datastore/key"
- }, {
- "title": "Query",
- "type": "datastore/query/query"
- }, {
- "title": "GQL Query",
- "type": "datastore/query/gqlquery"
- }, {
- "title": "GeoPoint",
- "type": "datastore/geopoint"
- }, {
- "title": "Blob",
- "type": "datastore/blob"
- }]
- },
- {
- "title": "Logging",
- "type": "logging/loggingclient",
- "nav": [{
- "title": "Entry",
- "type": "logging/entry"
- }, {
- "title": "Logger",
- "type": "logging/logger"
- }, {
- "title": "Metric",
- "type": "logging/metric"
- },{
- "title": "PsrLogger",
- "type": "logging/psrlogger"
- },{
- "title": "Sink",
- "type": "logging/sink"
- }]
- },
- {
- "title": "Natural Language",
- "type": "naturallanguage/naturallanguageclient",
- "nav": [{
- "title": "Annotation",
- "type": "naturallanguage/annotation"
- }]
- },
- {
- "title": "PubSub",
- "type": "pubsub/pubsubclient",
- "nav": [{
- "title": "Message",
- "type": "pubsub/message"
- },
- {
- "title": "Subscription",
- "type": "pubsub/subscription"
- },
- {
- "title": "Topic",
- "type": "pubsub/topic"
- }]
- },
- {
- "title": "Speech",
- "type": "speech/speechclient",
- "nav": [{
- "title": "Operation",
- "type": "speech/operation"
- }]
- },
- {
- "title": "Storage",
- "type": "storage/storageclient",
- "nav": [{
- "title": "ACL",
- "type": "storage/acl"
- }, {
- "title": "Bucket",
- "type": "storage/bucket"
- }, {
- "title": "StorageObject",
- "type": "storage/storageobject"
- }]
- },
- {
- "title": "Translate",
- "type": "translate/translateclient"
- },
- {
- "title": "Vision",
- "type": "vision/visionclient",
- "nav": [{
- "title": "Image",
- "type": "vision/image"
- }, {
- "title": "Annotation",
- "type": "vision/annotation"
- }]
- }
- ]
+ "services": []
}
diff --git a/phpcs-ruleset.xml b/phpcs-ruleset.xml
index ed53b7bbb602..d08453666091 100644
--- a/phpcs-ruleset.xml
+++ b/phpcs-ruleset.xml
@@ -4,5 +4,8 @@
src/*/V[0-9]+
+
+ src/Storage/StreamWrapper.php
+
src
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 8afd82d909fa..23718b678755 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -10,6 +10,8 @@
src
src/*/V[!a-zA-Z]*
+ src/*/*/V[!a-zA-Z]*
+ src/*/*/*/V[!a-zA-Z]*
diff --git a/src/BigQuery/BigQueryClient.php b/src/BigQuery/BigQueryClient.php
index 70c9aaba22b4..10b32efac47d 100644
--- a/src/BigQuery/BigQueryClient.php
+++ b/src/BigQuery/BigQueryClient.php
@@ -17,30 +17,23 @@
namespace Google\Cloud\BigQuery;
-use Google\Cloud\ArrayTrait;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
use Google\Cloud\BigQuery\Connection\Rest;
-use Google\Cloud\ClientTrait;
-use Google\Cloud\Int64;
+use Google\Cloud\Core\ClientTrait;
+use Google\Cloud\Core\Int64;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\StreamInterface;
/**
- * Google Cloud BigQuery client. Allows you to create, manage, share and query
- * data. Find more information at
+ * Google Cloud BigQuery allows you to create, manage, share and query data.
+ * Find more information at the
* [Google Cloud BigQuery Docs](https://cloud.google.com/bigquery/what-is-bigquery).
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- *
- * $bigQuery = $cloud->bigQuery();
- * ```
- *
- * ```
- * // BigQueryClient can be instantiated directly.
* use Google\Cloud\BigQuery\BigQueryClient;
*
* $bigQuery = new BigQueryClient();
@@ -52,6 +45,8 @@ class BigQueryClient
use ClientTrait;
use JobConfigurationTrait;
+ const VERSION = '0.1.0';
+
const SCOPE = 'https://www.googleapis.com/auth/bigquery';
const INSERT_SCOPE = 'https://www.googleapis.com/auth/bigquery.insertdata';
@@ -90,7 +85,7 @@ class BigQueryClient
* to** `3`.
* @type array $scopes Scopes to be used for the request.
* @type bool $returnInt64AsObject If true, 64 bit integers will be
- * returned as a {@see Google\Cloud\Int64} object for 32 bit
+ * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
* }
*/
@@ -123,7 +118,7 @@ public function __construct(array $config = [])
* | `\DateTimeInterface` | `DATETIME` |
* | {@see Google\Cloud\BigQuery\Bytes} | `BYTES` |
* | {@see Google\Cloud\BigQuery\Date} | `DATE` |
- * | {@see Google\Cloud\Int64} | `INT64` |
+ * | {@see Google\Cloud\Core\Int64} | `INT64` |
* | {@see Google\Cloud\BigQuery\Time} | `TIME` |
* | {@see Google\Cloud\BigQuery\Timestamp} | `TIMESTAMP` |
* | Associative Array | `STRUCT` |
@@ -358,35 +353,39 @@ public function job($id)
*
* @type bool $allUsers Whether to display jobs owned by all users in
* the project. **Defaults to** `false`.
- * @type int $maxResults Maximum number of results to return.
+ * @type int $maxResults Maximum number of results to return per page.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* @type string $stateFilter Filter for job state. Maybe be either
* `done`, `pending`, or `running`.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function jobs(array $options = [])
{
- $options['pageToken'] = null;
-
- do {
- $response = $this->connection->listJobs($options + ['projectId' => $this->projectId]);
-
- if (!isset($response['jobs'])) {
- return;
- }
-
- foreach ($response['jobs'] as $job) {
- yield new Job(
- $this->connection,
- $job['jobReference']['jobId'],
- $this->projectId,
- $job,
- $this->mapper
- );
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ $resultLimit = $this->pluck('resultLimit', $options, false);
+
+ return new ItemIterator(
+ new PageIterator(
+ function (array $job) {
+ return new Job(
+ $this->connection,
+ $job['jobReference']['jobId'],
+ $this->projectId,
+ $job,
+ $this->mapper
+ );
+ },
+ [$this->connection, 'listJobs'],
+ $options + ['project' => $this->projectId],
+ [
+ 'itemsKey' => 'jobs',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -404,7 +403,12 @@ public function jobs(array $options = [])
*/
public function dataset($id)
{
- return new Dataset($this->connection, $id, $this->projectId);
+ return new Dataset(
+ $this->connection,
+ $id,
+ $this->projectId,
+ $this->mapper
+ );
}
/**
@@ -425,32 +429,37 @@ public function dataset($id)
* Configuration options.
*
* @type bool $all Whether to list all datasets, including hidden ones.
- * @type int $maxResults Maximum number of results to return.
+ * @type int $maxResults Maximum number of results to return per page.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function datasets(array $options = [])
{
- $options['pageToken'] = null;
-
- do {
- $response = $this->connection->listDatasets($options + ['projectId' => $this->projectId]);
-
- if (!isset($response['datasets'])) {
- return;
- }
-
- foreach ($response['datasets'] as $dataset) {
- yield new Dataset(
- $this->connection,
- $dataset['datasetReference']['datasetId'],
- $this->projectId,
- $dataset
- );
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ $resultLimit = $this->pluck('resultLimit', $options, false);
+
+ return new ItemIterator(
+ new PageIterator(
+ function (array $dataset) {
+ return new Dataset(
+ $this->connection,
+ $dataset['datasetReference']['datasetId'],
+ $this->projectId,
+ $this->mapper,
+ $dataset
+ );
+ },
+ [$this->connection, 'listDatasets'],
+ $options + ['project' => $this->projectId],
+ [
+ 'itemsKey' => 'datasets',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -487,7 +496,13 @@ public function createDataset($id, array $options = [])
]
] + $options);
- return new Dataset($this->connection, $id, $this->projectId, $response);
+ return new Dataset(
+ $this->connection,
+ $id,
+ $this->projectId,
+ $this->mapper,
+ $response
+ );
}
/**
diff --git a/src/BigQuery/Bytes.php b/src/BigQuery/Bytes.php
index 5a09c5a407a8..571f23d7222f 100644
--- a/src/BigQuery/Bytes.php
+++ b/src/BigQuery/Bytes.php
@@ -26,10 +26,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\BigQuery\BigQueryClient;
*
- * $cloud = new ServiceBuilder();
- * $bigQuery = $cloud->bigQuery();
+ * $bigQuery = new BigQueryClient();
*
* $bytes = $bigQuery->bytes('hello world');
* ```
diff --git a/src/BigQuery/Connection/ConnectionInterface.php b/src/BigQuery/Connection/ConnectionInterface.php
index df0be5e51982..1350a66e2083 100644
--- a/src/BigQuery/Connection/ConnectionInterface.php
+++ b/src/BigQuery/Connection/ConnectionInterface.php
@@ -17,6 +17,8 @@
namespace Google\Cloud\BigQuery\Connection;
+use Google\Cloud\Core\Upload\AbstractUploader;
+
/**
* Represents a connection to
* [BigQuery](https://cloud.google.com/bigquery/).
diff --git a/src/BigQuery/Connection/Rest.php b/src/BigQuery/Connection/Rest.php
index 50651f555310..1118a56c0a59 100644
--- a/src/BigQuery/Connection/Rest.php
+++ b/src/BigQuery/Connection/Rest.php
@@ -17,14 +17,15 @@
namespace Google\Cloud\BigQuery\Connection;
+use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\RestTrait;
-use Google\Cloud\Upload\AbstractUploader;
-use Google\Cloud\Upload\MultipartUploader;
-use Google\Cloud\Upload\ResumableUploader;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\Upload\AbstractUploader;
+use Google\Cloud\Core\Upload\MultipartUploader;
+use Google\Cloud\Core\Upload\ResumableUploader;
+use Google\Cloud\Core\UriTrait;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
@@ -46,7 +47,8 @@ class Rest implements ConnectionInterface
public function __construct(array $config = [])
{
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/bigquery-v2.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/bigquery-v2.json',
+ 'componentVersion' => BigQueryClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
@@ -252,8 +254,9 @@ private function resolveUploadOptions(array $args)
unset($args['configuration']);
$uploaderOptionKeys = [
- 'httpOptions',
+ 'restOptions',
'retries',
+ 'requestTimeout',
'metadata'
];
diff --git a/src/BigQuery/Dataset.php b/src/BigQuery/Dataset.php
index fbc90ca4839e..27c86ef2c1d0 100644
--- a/src/BigQuery/Dataset.php
+++ b/src/BigQuery/Dataset.php
@@ -17,7 +17,10 @@
namespace Google\Cloud\BigQuery;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
/**
@@ -26,6 +29,8 @@
*/
class Dataset
{
+ use ArrayTrait;
+
/**
* @var ConnectionInterface $connection Represents a connection to BigQuery.
*/
@@ -41,6 +46,11 @@ class Dataset
*/
private $info;
+ /**
+ * @var ValueMapper Maps values between PHP and BigQuery.
+ */
+ private $mapper;
+
/**
* @param ConnectionInterface $connection Represents a connection to
* BigQuery.
@@ -48,10 +58,16 @@ class Dataset
* @param string $projectId The project's ID.
* @param array $info [optional] The dataset's metadata.
*/
- public function __construct(ConnectionInterface $connection, $id, $projectId, array $info = [])
- {
+ public function __construct(
+ ConnectionInterface $connection,
+ $id,
+ $projectId,
+ ValueMapper $mapper,
+ array $info = []
+ ) {
$this->connection = $connection;
$this->info = $info;
+ $this->mapper = $mapper;
$this->identity = [
'datasetId' => $id,
'projectId' => $projectId
@@ -137,11 +153,17 @@ public function update(array $metadata, array $options = [])
* ```
*
* @param string $id The id of the table to request.
- * @return Dataset
+ * @return Table
*/
public function table($id)
{
- return new Table($this->connection, $id, $this->identity['datasetId'], $this->identity['projectId']);
+ return new Table(
+ $this->connection,
+ $id,
+ $this->identity['datasetId'],
+ $this->identity['projectId'],
+ $this->mapper
+ );
}
/**
@@ -161,33 +183,38 @@ public function table($id)
* @param array $options [optional] {
* Configuration options.
*
- * @type int $maxResults Maximum number of results to return.
+ * @type int $maxResults Maximum number of results to return per page.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function tables(array $options = [])
{
- $options['pageToken'] = null;
+ $resultLimit = $this->pluck('resultLimit', $options, false);
- do {
- $response = $this->connection->listTables($options + $this->identity);
-
- if (!isset($response['tables'])) {
- return;
- }
-
- foreach ($response['tables'] as $table) {
- yield new Table(
- $this->connection,
- $table['tableReference']['tableId'],
- $this->identity['datasetId'],
- $this->identity['projectId'],
- $table
- );
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $table) {
+ return new Table(
+ $this->connection,
+ $table['tableReference']['tableId'],
+ $this->identity['datasetId'],
+ $this->identity['projectId'],
+ $this->mapper,
+ $table
+ );
+ },
+ [$this->connection, 'listTables'],
+ $options + $this->identity,
+ [
+ 'itemsKey' => 'tables',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -227,6 +254,7 @@ public function createTable($id, array $options = [])
$id,
$this->identity['datasetId'],
$this->identity['projectId'],
+ $this->mapper,
$response
);
}
diff --git a/src/BigQuery/Date.php b/src/BigQuery/Date.php
index 85d3f6fca4c3..ba5c1bad3a5d 100644
--- a/src/BigQuery/Date.php
+++ b/src/BigQuery/Date.php
@@ -23,10 +23,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\BigQuery\BigQueryClient;
*
- * $cloud = new ServiceBuilder();
- * $bigQuery = $cloud->bigQuery();
+ * $bigQuery = new BigQueryClient();
*
* $date = $bigQuery->date(new \DateTime('1995-02-04'));
* ```
diff --git a/src/BigQuery/Job.php b/src/BigQuery/Job.php
index 8ae4810ac1ca..eb963797301b 100644
--- a/src/BigQuery/Job.php
+++ b/src/BigQuery/Job.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\BigQuery;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
/**
@@ -134,7 +134,7 @@ public function cancel(array $options = [])
* @param array $options [optional] {
* Configuration options.
*
- * @type int $maxResults Maximum number of results to read.
+ * @type int $maxResults Maximum number of results to read per page.
* @type int $startIndex Zero-based index of the starting row.
* @type int $timeoutMs How long to wait for the query to complete, in
* milliseconds. **Defaults to** `10000` milliseconds (10 seconds).
diff --git a/src/BigQuery/LICENSE b/src/BigQuery/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/BigQuery/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/BigQuery/QueryResults.php b/src/BigQuery/QueryResults.php
index f835b2080198..ab20936d9b36 100644
--- a/src/BigQuery/QueryResults.php
+++ b/src/BigQuery/QueryResults.php
@@ -18,7 +18,9 @@
namespace Google\Cloud\BigQuery;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
-use Google\Cloud\Exception\GoogleException;
+use Google\Cloud\Core\Exception\GoogleException;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
/**
* QueryResults represent the result of a BigQuery SQL query. Read more at the
@@ -95,7 +97,7 @@ public function __construct(
* | `\DateTimeInterface` | `DATETIME` |
* | {@see Google\Cloud\BigQuery\Bytes} | `BYTES` |
* | {@see Google\Cloud\BigQuery\Date} | `DATE` |
- * | {@see Google\Cloud\Int64} | `INTEGER` |
+ * | {@see Google\Cloud\Core\Int64} | `INTEGER` |
* | {@see Google\Cloud\BigQuery\Time} | `TIME` |
* | {@see Google\Cloud\BigQuery\Timestamp} | `TIMESTAMP` |
* | Associative Array | `RECORD` |
@@ -119,7 +121,7 @@ public function __construct(
* ```
*
* @param array $options [optional] Configuration options.
- * @return array
+ * @return ItemIterator
* @throws GoogleException Thrown if the query has not yet completed.
*/
public function rows(array $options = [])
@@ -128,40 +130,37 @@ public function rows(array $options = [])
throw new GoogleException('The query has not completed yet.');
}
- if (!isset($this->info['rows'])) {
- return;
- }
-
$schema = $this->info['schema']['fields'];
- while (true) {
- $options['pageToken'] = isset($this->info['pageToken']) ? $this->info['pageToken'] : null;
-
- foreach ($this->info['rows'] as $row) {
- $mergedRow = [];
+ return new ItemIterator(
+ new PageIterator(
+ function (array $row) use ($schema) {
+ $mergedRow = [];
- if ($row === null) {
- continue;
- }
+ if ($row === null) {
+ return $mergedRow;
+ }
- if (!array_key_exists('f', $row)) {
- throw new GoogleException('Bad response - missing key "f" for a row.');
- }
+ if (!array_key_exists('f', $row)) {
+ throw new GoogleException('Bad response - missing key "f" for a row.');
+ }
- foreach ($row['f'] as $key => $value) {
- $fieldSchema = $schema[$key];
- $mergedRow[$fieldSchema['name']] = $this->mapper->fromBigQuery($value, $fieldSchema);
- }
+ foreach ($row['f'] as $key => $value) {
+ $fieldSchema = $schema[$key];
+ $mergedRow[$fieldSchema['name']] = $this->mapper->fromBigQuery($value, $fieldSchema);
+ }
- yield $mergedRow;
- }
-
- if (!$options['pageToken']) {
- return;
- }
-
- $this->info = $this->connection->getQueryResults($options + $this->identity);
- }
+ return $mergedRow;
+ },
+ [$this->connection, 'getQueryResults'],
+ $options + $this->identity,
+ [
+ 'itemsKey' => 'rows',
+ 'firstPage' => $this->info,
+ 'nextResultTokenKey' => 'pageToken'
+ ]
+ )
+ );
}
/**
@@ -232,7 +231,7 @@ public function info()
* @param array $options [optional] {
* Configuration options.
*
- * @type int $maxResults Maximum number of results to read.
+ * @type int $maxResults Maximum number of results to read per page.
* @type int $startIndex Zero-based index of the starting row.
* @type int $timeoutMs How long to wait for the query to complete, in
* milliseconds. **Defaults to** `10000` milliseconds (10 seconds).
diff --git a/src/BigQuery/README.md b/src/BigQuery/README.md
new file mode 100644
index 000000000000..ef19e237c568
--- /dev/null
+++ b/src/BigQuery/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP BigQuery
+
+> Idiomatic PHP client for [BigQuery](https://cloud.google.com/bigquery/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-bigquery/latest/bigquery/bigqueryclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-bigquery
+```
diff --git a/src/BigQuery/Table.php b/src/BigQuery/Table.php
index 1202226f66d7..3d5354bc6793 100644
--- a/src/BigQuery/Table.php
+++ b/src/BigQuery/Table.php
@@ -18,8 +18,12 @@
namespace Google\Cloud\BigQuery;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\Storage\StorageObject;
+use Psr\Http\Message\StreamInterface;
/**
* [BigQuery Tables](https://cloud.google.com/bigquery/docs/tables) are a
@@ -28,6 +32,7 @@
*/
class Table
{
+ use ArrayTrait;
use JobConfigurationTrait;
/**
@@ -45,18 +50,31 @@ class Table
*/
private $info;
+ /**
+ * @var ValueMapper Maps values between PHP and BigQuery.
+ */
+ private $mapper;
+
/**
* @param ConnectionInterface $connection Represents a connection to
* BigQuery.
* @param string $id The table's id.
* @param string $datasetId The dataset's id.
* @param string $projectId The project's id.
+ * @param ValueMapper $mapper Maps values between PHP and BigQuery.
* @param array $info [optional] The table's metadata.
*/
- public function __construct(ConnectionInterface $connection, $id, $datasetId, $projectId, array $info = [])
- {
+ public function __construct(
+ ConnectionInterface $connection,
+ $id,
+ $datasetId,
+ $projectId,
+ ValueMapper $mapper,
+ array $info = []
+ ) {
$this->connection = $connection;
$this->info = $info;
+ $this->mapper = $mapper;
$this->identity = [
'tableId' => $id,
'datasetId' => $datasetId,
@@ -137,7 +155,7 @@ public function update(array $metadata, array $options = [])
* $rows = $table->rows();
*
* foreach ($rows as $row) {
- * echo $row['name'];
+ * echo $row['name'] . PHP_EOL;
* }
* ```
*
@@ -146,35 +164,49 @@ public function update(array $metadata, array $options = [])
* @param array $options [optional] {
* Configuration options.
*
- * @type int $maxResults Maximum number of results to return.
+ * @type int $maxResults Maximum number of results to return per page.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* @type int $startIndex Zero-based index of the starting row.
* }
- * @return \Generator
+ * @return ItemIterator
+ * @throws GoogleException
*/
public function rows(array $options = [])
{
- $options['pageToken'] = null;
+ $resultLimit = $this->pluck('resultLimit', $options, false);
$schema = $this->info()['schema']['fields'];
- do {
- $response = $this->connection->listTableData($options + $this->identity);
-
- if (!isset($response['rows'])) {
- return;
- }
-
- foreach ($response['rows'] as $rows) {
- $row = [];
-
- foreach ($rows['f'] as $key => $field) {
- $row[$schema[$key]['name']] = $field['v'];
- }
-
- yield $row;
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $row) use ($schema) {
+ $mergedRow = [];
+
+ if ($row === null) {
+ return $mergedRow;
+ }
+
+ if (!array_key_exists('f', $row)) {
+ throw new GoogleException('Bad response - missing key "f" for a row.');
+ }
+
+ foreach ($row['f'] as $key => $value) {
+ $fieldSchema = $schema[$key];
+ $mergedRow[$fieldSchema['name']] = $this->mapper->fromBigQuery($value, $fieldSchema);
+ }
+
+ return $mergedRow;
+ },
+ [$this->connection, 'listTableData'],
+ $options + $this->identity,
+ [
+ 'itemsKey' => 'rows',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -229,7 +261,10 @@ public function copy(Table $destination, array $options = [])
*
* @see https://cloud.google.com/bigquery/docs/reference/v2/jobs Jobs insert API Documentation.
*
- * @param StorageObject $destination The destination object.
+ * @param string|StorageObject $destination The destination object. May be
+ * a {@see Google\Cloud\Storage\StorageObject} or a URI pointing to
+ * a Google Cloud Storage object in the format of
+ * `gs://{bucket-name}/{object-name}`.
* @param array $options [optional] {
* Configuration options.
*
@@ -239,15 +274,18 @@ public function copy(Table $destination, array $options = [])
* }
* @return Job
*/
- public function export(StorageObject $destination, array $options = [])
+ public function export($destination, array $options = [])
{
- $objIdentity = $destination->identity();
+ if ($destination instanceof StorageObject) {
+ $destination = $destination->gcsUri();
+ }
+
$config = $this->buildJobConfig(
'extract',
$this->identity['projectId'],
[
'sourceTable' => $this->identity,
- 'destinationUris' => ['gs://' . $objIdentity['bucket'] . '/' . $objIdentity['object']]
+ 'destinationUris' => [$destination]
],
$options
);
@@ -460,6 +498,10 @@ public function insertRows(array $rows, array $options = [])
throw new \InvalidArgumentException('A row must have a data key.');
}
+ foreach ($row['data'] as $key => $item) {
+ $row['data'][$key] = $this->mapper->toBigQuery($item);
+ }
+
$row['json'] = $row['data'];
unset($row['data']);
$options['rows'][] = $row;
diff --git a/src/BigQuery/Time.php b/src/BigQuery/Time.php
index 184282f8a456..42dc1935dca3 100644
--- a/src/BigQuery/Time.php
+++ b/src/BigQuery/Time.php
@@ -23,6 +23,10 @@
*
* Example:
* ```
+ * use Google\Cloud\BigQuery\BigQueryClient;
+ *
+ * $bigQuery = new BigQueryClient();
+ *
* $time = $bigQuery->time(new \DateTime('12:15:00.482172'));
* ```
*/
diff --git a/src/BigQuery/Timestamp.php b/src/BigQuery/Timestamp.php
index 76dcbcdf1ee3..88e9ad5ff34b 100644
--- a/src/BigQuery/Timestamp.php
+++ b/src/BigQuery/Timestamp.php
@@ -23,6 +23,10 @@
*
* Example:
* ```
+ * use Google\Cloud\BigQuery\BigQueryClient;
+ *
+ * $bigQuery = new BigQueryClient();
+ *
* $timestamp = $bigQuery->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z'));
* ```
*/
diff --git a/src/BigQuery/VERSION b/src/BigQuery/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/BigQuery/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/BigQuery/ValueMapper.php b/src/BigQuery/ValueMapper.php
index 0a2d26f65bb5..ba5fa6f9dc79 100644
--- a/src/BigQuery/ValueMapper.php
+++ b/src/BigQuery/ValueMapper.php
@@ -17,8 +17,8 @@
namespace Google\Cloud\BigQuery;
-use Google\Cloud\ArrayTrait;
-use Google\Cloud\Int64;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\Int64;
/**
* Maps values to their expected BigQuery types. This class is intended for
@@ -45,17 +45,18 @@ class ValueMapper
const TYPE_RECORD = 'RECORD';
const DATETIME_FORMAT = 'Y-m-d H:i:s.u';
+ const DATETIME_FORMAT_INSERT = 'Y-m-d\TH:i:s.u';
/**
* @var bool $returnInt64AsObject If true, 64 bit integers will be returned
- * as a {@see Google\Cloud\Int64} object for 32 bit platform
+ * as a {@see Google\Cloud\Core\Int64} object for 32 bit platform
* compatibility.
*/
private $returnInt64AsObject;
/**
* @param bool $returnInt64AsObject If true, 64 bit integers will be
- * returned as a {@see Google\Cloud\Int64} object for 32 bit
+ * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility.
*/
public function __construct($returnInt64AsObject)
@@ -105,10 +106,7 @@ public function fromBigQuery(array $value, array $schema)
case self::TYPE_TIME:
return new Time(new \DateTime($value));
case self::TYPE_TIMESTAMP:
- $timestamp = new \DateTime();
- $timestamp->setTimestamp((float) $value);
-
- return new Timestamp($timestamp);
+ return $this->timestampFromBigQuery($value);
case self::TYPE_RECORD:
return $this->recordFromBigQuery($value, $schema['fields']);
default:
@@ -121,6 +119,33 @@ public function fromBigQuery(array $value, array $schema)
}
}
+ /**
+ * Maps a user provided value to the expected BigQuery format.
+ *
+ * @param mixed $value The value to map.
+ * @return mixed
+ */
+ public function toBigQuery($value)
+ {
+ if ($value instanceof ValueInterface || $value instanceof Int64) {
+ return (string) $value;
+ }
+
+ if ($value instanceof \DateTime) {
+ return $value->format(self::DATETIME_FORMAT_INSERT);
+ }
+
+ if (is_array($value)) {
+ foreach ($value as $key => $item) {
+ $value[$key] = $this->toBigQuery($item);
+ }
+
+ return $value;
+ }
+
+ return $value;
+ }
+
/**
* Maps a value to the expected parameter format.
*
@@ -297,4 +322,48 @@ private function assocArrayToParameter(array $struct)
['structValues' => $values]
];
}
+
+ /**
+ * Converts a timestamp in string format received from BigQuery to a
+ * {@see Google\Cloud\BigQuery\Timestamp}.
+ *
+ * @param string $value The timestamp.
+ * @return Timestamp
+ */
+ private function timestampFromBigQuery($value)
+ {
+ // If the string contains 'E' convert from exponential notation to
+ // decimal notation. This doesn't cast to a float because precision can
+ // be lost.
+ if (strpos($value, 'E')) {
+ list($value, $exponent) = explode('E', $value);
+ list($firstDigit, $remainingDigits) = explode('.', $value);
+
+ if (strlen($remainingDigits) > $exponent) {
+ $value = $firstDigit . substr_replace($remainingDigits, '.', $exponent, 0);
+ } else {
+ $value = $firstDigit . str_pad($remainingDigits, $exponent, '0') . '.0';
+ }
+ }
+
+ list($unixTimestamp, $microSeconds) = explode('.', $value);
+ $dateTime = new \DateTime("@$unixTimestamp");
+
+ // If the timestamp is before the epoch, make sure we account for that
+ // before concatenating the microseconds.
+ if ($microSeconds > 0 && $unixTimestamp[0] === '-') {
+ $microSeconds = 1000000 - (int) str_pad($microSeconds, 6, '0');
+ $dateTime->modify('-1 second');
+ }
+
+ return new Timestamp(
+ new \DateTime(
+ sprintf(
+ '%s.%s+00:00',
+ $dateTime->format('Y-m-d H:i:s'),
+ $microSeconds
+ )
+ )
+ );
+ }
}
diff --git a/src/BigQuery/composer.json b/src/BigQuery/composer.json
new file mode 100644
index 000000000000..187c743879fa
--- /dev/null
+++ b/src/BigQuery/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "google/cloud-bigquery",
+ "description": "BigQuery Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "suggest": {
+ "google/cloud-storage": "Makes it easier to load data from Cloud Storage into BigQuery"
+ },
+ "extra": {
+ "component": {
+ "displayName": "Google Cloud BigQuery",
+ "id": "cloud-bigquery",
+ "target": "GoogleCloudPlatform/google-cloud-php-bigquery.git",
+ "path": "src/BigQuery",
+ "entry": "BigQueryClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\BigQuery\\": ""
+ }
+ }
+}
diff --git a/src/ArrayTrait.php b/src/Core/ArrayTrait.php
similarity index 82%
rename from src/ArrayTrait.php
rename to src/Core/ArrayTrait.php
index 0bb5d43b1f96..efca6404bf27 100644
--- a/src/ArrayTrait.php
+++ b/src/Core/ArrayTrait.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
/**
* Provides basic array helper methods.
@@ -78,4 +78,21 @@ private function isAssoc(array $arr)
{
return array_keys($arr) !== range(0, count($arr) - 1);
}
+
+ /**
+ * Just like array_filter(), but preserves falsey values except null.
+ *
+ * @param array $arr
+ * @return array
+ */
+ private function arrayFilterRemoveNull(array $arr)
+ {
+ return array_filter($arr, function ($element) {
+ if (!is_null($element)) {
+ return true;
+ }
+
+ return false;
+ });
+ }
}
diff --git a/src/CallTrait.php b/src/Core/CallTrait.php
similarity index 97%
rename from src/CallTrait.php
rename to src/Core/CallTrait.php
index 0ed942e76c43..dd7485f52134 100644
--- a/src/CallTrait.php
+++ b/src/Core/CallTrait.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
/**
* Provide magic method support for fetching values from results
diff --git a/src/ClientTrait.php b/src/Core/ClientTrait.php
similarity index 94%
rename from src/ClientTrait.php
rename to src/Core/ClientTrait.php
index e2f9e63b5256..4bf1083816d9 100644
--- a/src/ClientTrait.php
+++ b/src/Core/ClientTrait.php
@@ -15,14 +15,14 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
use Google\Auth\ApplicationDefaultCredentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\HttpHandler\HttpHandlerFactory;
-use Google\Cloud\Compute\Metadata;
-use Google\Cloud\Exception\GoogleException;
+use Google\Cloud\Core\Compute\Metadata;
+use Google\Cloud\Core\Exception\GoogleException;
use GuzzleHttp\Psr7;
/**
@@ -30,6 +30,8 @@
*/
trait ClientTrait
{
+ use JsonTrait;
+
/**
* @var string The project ID created in the Google Developers Console.
*/
@@ -114,9 +116,9 @@ private function getKeyFile(array $config = [])
throw new GoogleException('Given keyfile path does not exist');
}
- $keyFileData = json_decode(file_get_contents($config['keyFilePath']), true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
+ try {
+ $keyFileData = $this->jsonDecode(file_get_contents($config['keyFilePath']), true);
+ } catch (\InvalidArgumentException $ex) {
throw new GoogleException('Given keyfile was invalid');
}
diff --git a/src/Compute/Metadata.php b/src/Core/Compute/Metadata.php
similarity index 76%
rename from src/Compute/Metadata.php
rename to src/Core/Compute/Metadata.php
index 20118828d751..01dbd9267d40 100644
--- a/src/Compute/Metadata.php
+++ b/src/Core/Compute/Metadata.php
@@ -15,9 +15,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-namespace Google\Cloud\Compute;
+namespace Google\Cloud\Core\Compute;
-use Google\Cloud\Compute\Metadata\Readers\StreamReader;
+use Google\Cloud\Core\Compute\Metadata\Readers\StreamReader;
+use Google\Cloud\Core\Compute\Metadata\Readers\ReaderInterface;
/**
* A library for accessing the Google Compute Engine (GCE) metadata.
@@ -27,12 +28,17 @@
*
* You can get the GCE metadata values very easily like:
*
+ *
+ * Example:
* ```
- * use Google\Cloud\Compute\Metadata;
+ * use Google\Cloud\Core\Compute\Metadata;
*
* $metadata = new Metadata();
- * $project_id = $metadata->getProjectId();
+ * $projectId = $metadata->getProjectId();
+ * ```
*
+ * ```
+ * // It is easy to get any metadata from a project.
* $val = $metadata->getProjectMetadata($key);
* ```
*/
@@ -59,9 +65,9 @@ public function __construct()
/**
* Replace the default reader implementation
*
- * @param mixed $reader The reader implementation
+ * @param ReaderInterface $reader The reader implementation
*/
- public function setReader($reader)
+ public function setReader(ReaderInterface $reader)
{
$this->reader = $reader;
}
@@ -71,7 +77,7 @@ public function setReader($reader)
*
* Example:
* ```
- * $projectId = $reader->get('project/project-id');
+ * $projectId = $metadata->get('project/project-id');
* ```
*
* @param string $path The path of the item to retrieve.
@@ -86,15 +92,15 @@ public function get($path)
*
* Example:
* ```
- * $projectId = $reader->getProjectId();
+ * $projectId = $metadata->getProjectId();
* ```
*
* @return string
*/
public function getProjectId()
{
- if (! isset($this->projectId)) {
- $this->projectId = $this->reader->read('project/project-id');
+ if (!isset($this->projectId)) {
+ $this->projectId = $this->get('project/project-id');
}
return $this->projectId;
@@ -105,7 +111,7 @@ public function getProjectId()
*
* Example:
* ```
- * $foo = $reader->getProjectMetadata('foo');
+ * $foo = $metadata->getProjectMetadata('foo');
* ```
*
* @param string $key The metadata key
@@ -122,7 +128,7 @@ public function getProjectMetadata($key)
*
* Example:
* ```
- * $foo = $reader->getInstanceMetadata('foo');
+ * $foo = $metadata->getInstanceMetadata('foo');
* ```
*
* @param string $key The instance metadata key
diff --git a/src/Core/Compute/Metadata/Readers/ReaderInterface.php b/src/Core/Compute/Metadata/Readers/ReaderInterface.php
new file mode 100644
index 000000000000..4f5f6f507d0f
--- /dev/null
+++ b/src/Core/Compute/Metadata/Readers/ReaderInterface.php
@@ -0,0 +1,29 @@
+options, function ($metadataItem) {
+ if (array_key_exists('retryDelay', $metadataItem)) {
+ return true;
+ }
+
+ return false;
+ });
+
+ $delay = $metadata[0]['retryDelay'];
+ if (!isset($delay['seconds'])) {
+ $delay['seconds'] = 0;
+ }
+
+ return $delay;
+ }
+}
diff --git a/src/Exception/BadRequestException.php b/src/Core/Exception/BadRequestException.php
similarity index 95%
rename from src/Exception/BadRequestException.php
rename to src/Core/Exception/BadRequestException.php
index 7c0f71f020ed..52fcc88a0be4 100644
--- a/src/Exception/BadRequestException.php
+++ b/src/Core/Exception/BadRequestException.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Exception;
+namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a request fails due to an error in the request.
diff --git a/src/Exception/ConflictException.php b/src/Core/Exception/ConflictException.php
similarity index 95%
rename from src/Exception/ConflictException.php
rename to src/Core/Exception/ConflictException.php
index 270c6ed3a4c3..aa127a41d637 100644
--- a/src/Exception/ConflictException.php
+++ b/src/Core/Exception/ConflictException.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Exception;
+namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a request fails due to a conflict.
diff --git a/src/Core/Exception/FailedPreconditionException.php b/src/Core/Exception/FailedPreconditionException.php
new file mode 100644
index 000000000000..5c001c431e06
--- /dev/null
+++ b/src/Core/Exception/FailedPreconditionException.php
@@ -0,0 +1,27 @@
+serviceException = $serviceException;
+ $this->options = $options;
parent::__construct($message, $code);
}
diff --git a/src/ExponentialBackoff.php b/src/Core/ExponentialBackoff.php
similarity index 99%
rename from src/ExponentialBackoff.php
rename to src/Core/ExponentialBackoff.php
index 905c549819e6..8e34fe62a0ca 100644
--- a/src/ExponentialBackoff.php
+++ b/src/Core/ExponentialBackoff.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
/**
* Exponential backoff implementation.
diff --git a/src/GrpcRequestWrapper.php b/src/Core/GrpcRequestWrapper.php
similarity index 74%
rename from src/GrpcRequestWrapper.php
rename to src/Core/GrpcRequestWrapper.php
index a76ea808d574..ad42cf7d85ea 100644
--- a/src/GrpcRequestWrapper.php
+++ b/src/Core/GrpcRequestWrapper.php
@@ -15,16 +15,18 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
+use DrSlump\Protobuf\Codec\Binary;
use DrSlump\Protobuf\Codec\CodecInterface;
use DrSlump\Protobuf\Message;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\HttpHandler\HttpHandlerFactory;
-use Google\Cloud\Exception;
-use Google\Cloud\PhpArray;
-use Google\Cloud\RequestWrapperTrait;
+use Google\Cloud\Core\Exception;
+use Google\Cloud\Core\PhpArray;
+use Google\Cloud\Core\RequestWrapperTrait;
use Google\GAX\ApiException;
+use Google\GAX\OperationResponse;
use Google\GAX\PagedListResponse;
use Google\GAX\RetrySettings;
use Grpc;
@@ -47,6 +49,11 @@ class GrpcRequestWrapper
*/
private $codec;
+ /**
+ * @var CodecInterface A codec used for binary deserialization.
+ */
+ private $binaryCodec;
+
/**
* @var array gRPC specific configuration options passed off to the GAX
* library.
@@ -63,11 +70,18 @@ class GrpcRequestWrapper
Grpc\STATUS_DATA_LOSS
];
+ /**
+ * @var array Map of error metadata types to RPC wrappers.
+ */
+ private $metadataTypes = [
+ 'google.rpc.retryinfo-bin' => \google\rpc\RetryInfo::class
+ ];
+
/**
* @param array $config [optional] {
* Configuration options. Please see
- * {@see Google\Cloud\RequestWrapperTrait::setCommonDefaults()} for the other
- * available options.
+ * {@see Google\Cloud\Core\RequestWrapperTrait::setCommonDefaults()} for
+ * the other available options.
*
* @type callable $authHttpHandler A handler used to deliver Psr7
* requests specifically for authentication.
@@ -88,6 +102,7 @@ public function __construct(array $config = [])
$this->authHttpHandler = $config['authHttpHandler'] ?: HttpHandlerFactory::build();
$this->codec = $config['codec'];
$this->grpcOptions = $config['grpcOptions'];
+ $this->binaryCodec = new Binary;
}
/**
@@ -98,6 +113,8 @@ public function __construct(array $config = [])
* @param array $options [optional] {
* Request options.
*
+ * @type float $requestTimeout Seconds to wait before timing out the
+ * request. **Defaults to** `60`.
* @type int $retries Number of retries for a failed request.
* **Defaults to** `3`.
* @type array $grpcOptions gRPC specific configuration options.
@@ -108,6 +125,7 @@ public function send(callable $request, array $args, array $options = [])
{
$retries = isset($options['retries']) ? $options['retries'] : $this->retries;
$grpcOptions = isset($options['grpcOptions']) ? $options['grpcOptions'] : $this->grpcOptions;
+ $timeout = isset($options['requestTimeout']) ? $options['requestTimeout'] : $this->requestTimeout;
$backoff = new ExponentialBackoff($retries, function (\Exception $ex) {
$statusCode = $ex->getCode();
@@ -122,12 +140,16 @@ public function send(callable $request, array $args, array $options = [])
$grpcOptions['retrySettings'] = new RetrySettings(null, null);
}
+ if ($timeout && !array_key_exists('timeoutMs', $grpcOptions)) {
+ $grpcOptions['timeoutMs'] = $timeout * 1000;
+ }
+
$optionalArgs = &$args[count($args) - 1];
$optionalArgs += $grpcOptions;
try {
return $this->handleResponse($backoff->execute($request, $args));
- } catch (\Exception $ex) {
+ } catch (ApiException $ex) {
throw $this->convertToGoogleException($ex);
}
}
@@ -148,6 +170,10 @@ private function handleResponse($response)
return $response->serialize($this->codec);
}
+ if ($response instanceof OperationResponse) {
+ return $response;
+ }
+
return null;
}
@@ -172,6 +198,10 @@ private function convertToGoogleException(ApiException $ex)
$exception = Exception\ConflictException::class;
break;
+ case Grpc\STATUS_FAILED_PRECONDITION:
+ $exception = Exception\FailedPreconditionException::class;
+ break;
+
case Grpc\STATUS_UNKNOWN:
$exception = Exception\ServerException::class;
break;
@@ -180,11 +210,28 @@ private function convertToGoogleException(ApiException $ex)
$exception = Exception\ServerException::class;
break;
+ case Grpc\STATUS_ABORTED:
+ $exception = Exception\AbortedException::class;
+ break;
+
default:
$exception = Exception\ServiceException::class;
break;
}
- return new $exception($ex->getMessage(), $ex->getCode(), $ex);
+ $metadata = [];
+ if ($ex->getMetadata()) {
+ foreach ($ex->getMetadata() as $type => $binaryValue) {
+ if (!isset($this->metadataTypes[$type])) {
+ continue;
+ }
+
+ $metadata[] = (new $this->metadataTypes[$type])
+ ->deserialize($binaryValue[0], $this->binaryCodec)
+ ->serialize($this->codec);
+ }
+ }
+
+ return new $exception($ex->getMessage(), $ex->getCode(), $ex, $metadata);
}
}
diff --git a/src/GrpcTrait.php b/src/Core/GrpcTrait.php
similarity index 79%
rename from src/GrpcTrait.php
rename to src/Core/GrpcTrait.php
index ffc0fcd6136e..8a2b59cdb32a 100644
--- a/src/GrpcTrait.php
+++ b/src/Core/GrpcTrait.php
@@ -15,15 +15,14 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
use DateTime;
use DateTimeZone;
use Google\Auth\FetchAuthTokenCache;
use Google\Auth\Cache\MemoryCacheItemPool;
-use Google\Cloud\ArrayTrait;
-use Google\Cloud\GrpcRequestWrapper;
-use Google\Cloud\ServiceBuilder;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\GrpcRequestWrapper;
/**
* Provides shared functionality for gRPC service implementations.
@@ -57,12 +56,13 @@ public function setRequestWrapper(GrpcRequestWrapper $requestWrapper)
*/
public function send(callable $request, array $args)
{
- $requestOptions = $args[count($args) - 1];
+ $requestOptions = $this->pluckArray([
+ 'grpcOptions',
+ 'retries',
+ 'requestTimeout'
+ ], $args[count($args) - 1]);
- return $this->requestWrapper->send($request, $args, array_intersect_key($requestOptions, [
- 'grpcOptions' => null,
- 'retries' => null
- ]));
+ return $this->requestWrapper->send($request, $args, $requestOptions);
}
/**
@@ -70,13 +70,13 @@ public function send(callable $request, array $args)
*
* @return array
*/
- private function getGaxConfig()
+ private function getGaxConfig($version)
{
return [
'credentialsLoader' => $this->requestWrapper->getCredentialsFetcher(),
'enableCaching' => false,
- 'appName' => 'gcloud-php',
- 'appVersion' => ServiceBuilder::VERSION
+ 'libName' => 'gccl',
+ 'libVersion' => $version
];
}
@@ -178,4 +178,24 @@ private function formatValueForApi($value)
return ['list_value' => $this->formatListForApi($value)];
}
}
+
+ /**
+ * Format a timestamp for the API with nanosecond precision.
+ *
+ * @param string $value
+ * @return array
+ */
+ private function formatTimestampForApi($value)
+ {
+ preg_match('/\.(\d{1,9})Z/', $value, $matches);
+ $value = preg_replace('/\.(\d{1,9})Z/', '.000000Z', $value);
+
+ $dt = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', $value);
+ $nanos = (isset($matches[1])) ? $matches[1] : 0;
+
+ return [
+ 'seconds' => (int)$dt->format('U'),
+ 'nanos' => (int)$nanos
+ ];
+ }
}
diff --git a/src/Iam/Iam.php b/src/Core/Iam/Iam.php
similarity index 89%
rename from src/Iam/Iam.php
rename to src/Core/Iam/Iam.php
index 70708fdd68f6..2912f6e40042 100644
--- a/src/Iam/Iam.php
+++ b/src/Core/Iam/Iam.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Iam;
+namespace Google\Cloud\Core\Iam;
/**
* IAM Manager
@@ -26,8 +26,8 @@
* Note that examples make use of the PubSub API, and the
* {@see Google\Cloud\PubSub\Topic} class.
*
- * Policies can be created using the {@see Google\Cloud\Iam\PolicyBuilder} to
- * help ensure their validity.
+ * Policies can be created using the {@see Google\Cloud\Core\Iam\PolicyBuilder}
+ * to help ensure their validity.
*
* Example:
* ```
@@ -35,10 +35,9 @@
* // In this example, we'll use PubSub topics to demonstrate
* // how IAM politices are managed.
*
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\PubSub\PubSubClient;
*
- * $cloud = new ServiceBuilder;
- * $pubsub = $cloud->pubsub();
+ * $pubsub = new PubSubClient();
* $topic = $pubsub->topic('my-new-topic');
*
* $iam = $topic->iam();
@@ -76,7 +75,8 @@ public function __construct(IamConnectionInterface $connection, $resource)
* Get the existing IAM policy for this resource.
*
* If a policy has already been retrieved from the API, it will be returned.
- * To fetch a fresh copy of the policy, use {@see Google\Cloud\Iam\Iam::reload()}.
+ * To fetch a fresh copy of the policy, use
+ * {@see Google\Cloud\Core\Iam\Iam::reload()}.
*
* Example:
* ```
@@ -126,9 +126,6 @@ public function setPolicy(array $policy, array $options = [])
*
* Invalid permissions will raise a BadRequestException.
*
- * A list of allowed permissions can be found in the
- * [access control documentation](https://cloud.google.com/pubsub/access_control#permissions).
- *
* Example:
* ```
* $allowedPermissions = $iam->testPermissions([
diff --git a/src/Iam/IamConnectionInterface.php b/src/Core/Iam/IamConnectionInterface.php
similarity index 97%
rename from src/Iam/IamConnectionInterface.php
rename to src/Core/Iam/IamConnectionInterface.php
index f475df94532f..6b15166ad763 100644
--- a/src/Iam/IamConnectionInterface.php
+++ b/src/Core/Iam/IamConnectionInterface.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Iam;
+namespace Google\Cloud\Core\Iam;
/**
* An interface defining how wrappers interact with their IAM implementations.
diff --git a/src/Iam/PolicyBuilder.php b/src/Core/Iam/PolicyBuilder.php
similarity index 97%
rename from src/Iam/PolicyBuilder.php
rename to src/Core/Iam/PolicyBuilder.php
index 9a33fa0ecc3a..405b37ee90b2 100644
--- a/src/Iam/PolicyBuilder.php
+++ b/src/Core/Iam/PolicyBuilder.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Iam;
+namespace Google\Cloud\Core\Iam;
use InvalidArgumentException;
@@ -24,7 +24,7 @@
*
* Example:
* ```
- * use Google\Cloud\Iam\PolicyBuilder;
+ * use Google\Cloud\Core\Iam\PolicyBuilder;
*
* $builder = new PolicyBuilder();
* $builder->addBinding('roles/admin', [ 'user:admin@domain.com' ]);
@@ -80,7 +80,7 @@ public function __construct(array $policy = [])
* ```
* $builder->setBindings([
* [
- * 'role' => roles/admin',
+ * 'role' => 'roles/admin',
* 'members' => [
* 'user:admin@domain.com'
* ]
diff --git a/src/Int64.php b/src/Core/Int64.php
similarity index 98%
rename from src/Int64.php
rename to src/Core/Int64.php
index ee963118bcac..442dfeadd08e 100644
--- a/src/Int64.php
+++ b/src/Core/Int64.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
/**
* Represents a 64 bit integer. This can be useful when working on a 32 bit
diff --git a/src/Core/Iterator/ItemIterator.php b/src/Core/Iterator/ItemIterator.php
new file mode 100644
index 000000000000..8c09be7d9702
--- /dev/null
+++ b/src/Core/Iterator/ItemIterator.php
@@ -0,0 +1,26 @@
+pageIterator = $pageIterator;
+ }
+
+ /**
+ * Fetch the token used to get the next set of results.
+ *
+ * @return string|null
+ */
+ public function nextResultToken()
+ {
+ return $this->pageIterator->nextResultToken();
+ }
+
+ /**
+ * Iterate over the results on a per page basis.
+ *
+ * @return \Iterator
+ */
+ public function iterateByPage()
+ {
+ return $this->pageIterator;
+ }
+
+ /**
+ * Rewind the iterator.
+ *
+ * @return null
+ */
+ public function rewind()
+ {
+ $this->pageIndex = 0;
+ $this->position = 0;
+ $this->pageIterator->rewind();
+ }
+
+ /**
+ * Get the current item.
+ *
+ * @return array|null
+ */
+ public function current()
+ {
+ $page = $this->pageIterator->current();
+
+ return isset($page[$this->pageIndex])
+ ? $page[$this->pageIndex]
+ : null;
+ }
+
+ /**
+ * Get the key current item's key.
+ *
+ * @return int
+ */
+ public function key()
+ {
+ return $this->position;
+ }
+
+ /**
+ * Advances to the next item.
+ *
+ * @return null
+ */
+ public function next()
+ {
+ $this->pageIndex++;
+ $this->position++;
+
+ if (count($this->pageIterator->current()) <= $this->pageIndex && $this->pageIterator->nextResultToken()) {
+ $this->pageIterator->next();
+ $this->pageIndex = 0;
+ }
+ }
+
+ /**
+ * Determines if the current position is valid.
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ if (isset($this->pageIterator->current()[$this->pageIndex])) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/Core/Iterator/PageIterator.php b/src/Core/Iterator/PageIterator.php
new file mode 100644
index 000000000000..53c97aecaa62
--- /dev/null
+++ b/src/Core/Iterator/PageIterator.php
@@ -0,0 +1,26 @@
+resultMapper = $resultMapper;
+ $this->call = $call;
+ $this->config = $config + [
+ 'itemsKey' => 'items',
+ 'nextResultTokenKey' => 'nextPageToken',
+ 'resultTokenKey' => 'pageToken',
+ 'firstPage' => null,
+ 'resultLimit' => 0,
+ 'setNextResultTokenCondition' => function () {
+ return true;
+ }
+ ];
+ $this->callOptions = $callOptions;
+ $this->resultTokenPath = explode('.', $this->config['resultTokenKey']);
+ $this->nextResultTokenPath = explode('.', $this->config['nextResultTokenKey']);
+ $this->itemsPath = explode('.', $this->config['itemsKey']);
+ $this->initialResultToken = $this->nextResultToken();
+ }
+
+ /**
+ * Fetch the token used to get the next set of results.
+ *
+ * @return string|null
+ */
+ public function nextResultToken()
+ {
+ return $this->get($this->resultTokenPath, $this->callOptions);
+ }
+
+ /**
+ * Rewind the iterator.
+ *
+ * @return null
+ */
+ public function rewind()
+ {
+ $this->itemCount = 0;
+ $this->position = 0;
+
+ if ($this->config['firstPage']) {
+ list($this->page, $shouldContinue) = $this->mapResults($this->config['firstPage']);
+ $nextResultToken = $this->determineNextResultToken($this->page, $shouldContinue);
+ } else {
+ $this->page = null;
+ $nextResultToken = $this->initialResultToken;
+ }
+
+ if ($nextResultToken) {
+ $this->set($this->resultTokenPath, $this->callOptions, $nextResultToken);
+ }
+ }
+
+ /**
+ * Get the current page.
+ *
+ * @return array|null
+ */
+ public function current()
+ {
+ if (!$this->page) {
+ $this->page = $this->executeCall();
+ }
+
+ return $this->get($this->itemsPath, $this->page);
+ }
+
+ /**
+ * Get the key current page's key.
+ *
+ * @return int
+ */
+ public function key()
+ {
+ return $this->position;
+ }
+
+ /**
+ * Advances to the next page.
+ *
+ * @return null
+ */
+ public function next()
+ {
+ $this->position++;
+
+ if ($this->nextResultToken()) {
+ $this->page = $this->executeCall();
+ } else {
+ $this->page = null;
+ }
+ }
+
+ /**
+ * Determines if the current position is valid.
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ if (!$this->page && $this->position) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Executes the provided call to get a set of results.
+ *
+ * @return array
+ */
+ private function executeCall()
+ {
+ $call = $this->call;
+ list($results, $shouldContinue) = $this->mapResults(
+ $call($this->callOptions)
+ );
+
+ $this->set(
+ $this->resultTokenPath,
+ $this->callOptions,
+ $this->determineNextResultToken($results, $shouldContinue)
+ );
+
+ return $results;
+ }
+
+ /**
+ * @param array $results
+ * @return array
+ */
+ private function mapResults(array $results)
+ {
+ $items = $this->get($this->itemsPath, $results);
+ $resultMapper = $this->resultMapper;
+ $shouldContinue = true;
+
+ if ($items) {
+ foreach ($items as $key => $item) {
+ $items[$key] = $resultMapper($item);
+ $this->itemCount++;
+
+ if ($this->config['resultLimit'] && $this->config['resultLimit'] <= $this->itemCount) {
+ $items = array_slice($items, 0, $key + 1);
+ $shouldContinue = false;
+ break;
+ }
+ }
+
+ $this->set($this->itemsPath, $results, $items);
+ }
+
+ return [$results, $shouldContinue];
+ }
+
+ /**
+ * @param array $results
+ * @param bool $shouldContinue
+ * @return null
+ */
+ private function determineNextResultToken(array $results, $shouldContinue = true)
+ {
+ return $shouldContinue && $this->config['setNextResultTokenCondition']($results)
+ ? $this->get($this->nextResultTokenPath, $results)
+ : null;
+ }
+
+ /**
+ * @param array $path
+ * @param array $array
+ * @return mixed
+ */
+ private function get(array $path, array $array)
+ {
+ $temp = &$array;
+
+ foreach ($path as $key) {
+ $temp = &$temp[$key];
+ }
+
+ return $temp;
+ }
+
+ /**
+ * @param array $path
+ * @param array $array
+ * @param mixed $value
+ * @return null
+ */
+ private function set(array $path, array &$array, $value)
+ {
+ $temp = &$array;
+
+ foreach ($path as $key) {
+ $temp = &$temp[$key];
+ }
+
+ $temp = $value;
+ }
+}
diff --git a/src/Core/JsonTrait.php b/src/Core/JsonTrait.php
new file mode 100644
index 000000000000..84748d7dbf0c
--- /dev/null
+++ b/src/Core/JsonTrait.php
@@ -0,0 +1,68 @@
+jsonEncode($payload);
}
}
diff --git a/src/Logger/AppEngineFlexHandler.php b/src/Core/Logger/AppEngineFlexHandler.php
similarity index 98%
rename from src/Logger/AppEngineFlexHandler.php
rename to src/Core/Logger/AppEngineFlexHandler.php
index 35c121d77890..78b5665f4038 100644
--- a/src/Logger/AppEngineFlexHandler.php
+++ b/src/Core/Logger/AppEngineFlexHandler.php
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-namespace Google\Cloud\Logger;
+namespace Google\Cloud\Core\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\StreamHandler;
diff --git a/src/Core/LongRunning/LROTrait.php b/src/Core/LongRunning/LROTrait.php
new file mode 100644
index 000000000000..0b965357231e
--- /dev/null
+++ b/src/Core/LongRunning/LROTrait.php
@@ -0,0 +1,45 @@
+spanner();
+ * $instance = $spanner->instance('my-instance');
+ *
+ * $operation = $instance->createDatabase('my-database');
+ * ```
+ */
+class LongRunningOperation
+{
+ const WAIT_INTERVAL = 1.0;
+
+ const STATE_IN_PROGRESS = 'inProgress';
+ const STATE_SUCCESS = 'success';
+ const STATE_ERROR = 'error';
+
+ /**
+ * @var LongRunningConnectionInterface
+ */
+ private $connection;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var array
+ */
+ private $info;
+
+ /**
+ * @var array|null
+ */
+ private $result;
+
+ /**
+ * @var array|null
+ */
+ private $error;
+
+ /**
+ * @var array
+ */
+ private $callablesMap;
+
+ /**
+ * @param LongRunningConnectionInterface $connection An implementation
+ * mapping to methods which handle LRO resolution in the service.
+ * @param string $name The Operation name.
+ * @param array $callablesMap An collection of form [(string) type, (callable) callable]
+ * providing a function to invoke when an operation completes. The
+ * callable Type should correspond to an expected value of
+ * operation.metadata.typeUrl.
+ */
+ public function __construct(
+ LongRunningConnectionInterface $connection,
+ $name,
+ array $callablesMap
+ ) {
+ $this->connection = $connection;
+ $this->name = $name;
+ $this->callablesMap = $callablesMap;
+ }
+
+ /**
+ * Return the Operation name.
+ *
+ * Example:
+ * ```
+ * $name = $operation->name();
+ * ```
+ *
+ * @return string
+ */
+ public function name()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Check if the Operation is done.
+ *
+ * Example:
+ * ```
+ * if ($operation->done()) {
+ * echo "The operation is done!";
+ * }
+ * ```
+ *
+ * @return bool
+ */
+ public function done()
+ {
+ return (isset($this->info()['done']))
+ ? $this->info['done']
+ : false;
+ }
+
+ /**
+ * Get the state of the Operation.
+ *
+ * Return value will be one of `LongRunningOperation::STATE_IN_PROGRESS`,
+ * `LongRunningOperation::STATE_SUCCESS` or
+ * `LongRunningOperation::STATE_ERROR`.
+ *
+ * Example:
+ * ```
+ * switch ($operation->state()) {
+ * case LongRunningOperation::STATE_IN_PROGRESS:
+ * echo "Operation is in progress";
+ * break;
+ *
+ * case LongRunningOperation::STATE_SUCCESS:
+ * echo "Operation succeeded";
+ * break;
+ *
+ * case LongRunningOperation::STATE_ERROR:
+ * echo "Operation failed";
+ * break;
+ * }
+ * ```
+ *
+ * @return string
+ */
+ public function state()
+ {
+ if (!$this->done()) {
+ return self::STATE_IN_PROGRESS;
+ }
+
+ if (isset($this->info['response'])) {
+ return self::STATE_SUCCESS;
+ }
+
+ return self::STATE_ERROR;
+ }
+
+ /**
+ * Get the Operation result.
+ *
+ * The return type of this method is dictated by the type of Operation.
+ *
+ * Returns null if the Operation is not yet complete, or if an error occurred.
+ *
+ * Note that if the Operation has not yet been reloaded, this may return
+ * null even when the operation has completed. Use
+ * {@see Google\Cloud\LongRunning\LongRunningOperation::reload()} to get the
+ * Operation state before retrieving the result.
+ *
+ * Example:
+ * ```
+ * $result = $operation->result();
+ * ```
+ *
+ * @return mixed|null
+ */
+ public function result()
+ {
+ $this->info();
+ return $this->result;
+ }
+
+ /**
+ * Get the Operation error.
+ *
+ * Returns null if the Operation is not yet complete, or if no error occurred.
+ *
+ * Note that if the Operation has not yet been reloaded, this may return
+ * null even when the operation has completed. Use
+ * {@see Google\Cloud\LongRunning\LongRunningOperation::reload()} to get the
+ * Operation state before retrieving the result.
+ *
+ * Example:
+ * ```
+ * $error = $operation->error();
+ * ```
+ *
+ * @return array|null
+ */
+ public function error()
+ {
+ $this->info();
+ return $this->error;
+ }
+
+ /**
+ * Get the Operation info.
+ *
+ * If the Operation has not been checked previously, a service call will be
+ * executed.
+ *
+ * Example:
+ * ```
+ * $info = $operation->info();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @param array $options Configuration options.
+ * @return array [google.longrunning.Operation](https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operation)
+ * @codingStandardsIgnoreEnd
+ */
+ public function info(array $options = [])
+ {
+ return $this->info ?: $this->reload($options);
+ }
+
+ /**
+ * Reload the Operation to check its status.
+ *
+ * Example:
+ * ```
+ * $operation->reload();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @param array $options Configuration Options.
+ * @return array [google.longrunning.Operation](https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operation)
+ * @codingStandardsIgnoreEnd
+ */
+ public function reload(array $options = [])
+ {
+ $res = $this->connection->get([
+ 'name' => $this->name,
+ ] + $options);
+
+ if (isset($res['done']) && $res['done']) {
+ $type = $res['metadata']['typeUrl'];
+ $this->result = $this->executeDoneCallback($type, $res['response']);
+ $this->error = (isset($res['error']))
+ ? $res['error']
+ : null;
+ }
+
+ return $this->info = $res;
+ }
+
+ /**
+ * Reload the operation until it is complete.
+ *
+ * The return type of this method is dictated by the type of Operation. If
+ * `$options.maxPollingDurationSeconds` is set, and the poll exceeds the
+ * limit, the return will be `null`.
+ *
+ * Example:
+ * ```
+ * $result = $operation->pollUntilComplete();
+ * ```
+ *
+ * @param array $options {
+ * Configuration Options
+ *
+ * @type float $pollingIntervalSeconds The polling interval to use, in
+ * seconds. **Defaults to** `1.0`.
+ * @type float $maxPollingDurationSeconds The maximum amount of time to
+ * continue polling. **Defaults to** `0.0`.
+ * }
+ * @return mixed|null
+ */
+ public function pollUntilComplete(array $options = [])
+ {
+ $options += [
+ 'pollingIntervalSeconds' => $this::WAIT_INTERVAL,
+ 'maxPollingDurationSeconds' => 0.0,
+ ];
+
+ $pollingIntervalMicros = $options['pollingIntervalSeconds'] * 1000000;
+ $maxPollingDuration = $options['maxPollingDurationSeconds'];
+ $hasMaxPollingDuration = $maxPollingDuration > 0.0;
+ $endTime = microtime(true) + $maxPollingDuration;
+
+ do {
+ usleep($pollingIntervalMicros);
+ $this->reload($options);
+ } while (!$this->done() && (!$hasMaxPollingDuration || microtime(true) < $endTime));
+
+ return $this->result;
+ }
+
+ /**
+ * Cancel a Long Running Operation.
+ *
+ * Example:
+ * ```
+ * $operation->cancel();
+ * ```
+ *
+ * @param array $options Configuration options.
+ * @return void
+ */
+ public function cancel(array $options = [])
+ {
+ $this->connection->cancel([
+ 'name' => $this->name
+ ]);
+ }
+
+ /**
+ * Delete a Long Running Operation.
+ *
+ * Example:
+ * ```
+ * $operation->delete();
+ * ```
+ *
+ * @param array $options Configuration Options.
+ * @return void
+ */
+ public function delete(array $options = [])
+ {
+ $this->connection->delete([
+ 'name' => $this->name
+ ]);
+ }
+
+ /**
+ * When the Operation is complete, there may be a callback enqueued to
+ * handle the response. If so, execute it and return the result.
+ *
+ * @param string $type The response type.
+ * @param mixed $response The response data.
+ * @return mixed
+ */
+ private function executeDoneCallback($type, $response)
+ {
+ if (is_null($response)) {
+ return null;
+ }
+
+ $callables = array_filter($this->callablesMap, function ($callable) use ($type) {
+ return $callable['typeUrl'] === $type;
+ });
+
+ if (count($callables) === 0) {
+ return $response;
+ }
+
+ $callable = current($callables);
+ $fn = $callable['callable'];
+
+ return call_user_func($fn, $response);
+ }
+
+ /**
+ * @access private
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'connection' => get_class($this->connection),
+ 'name' => $this->name,
+ 'callablesMap' => array_keys($this->callablesMap)
+ ];
+ }
+}
diff --git a/src/Core/LongRunning/OperationResponseTrait.php b/src/Core/LongRunning/OperationResponseTrait.php
new file mode 100644
index 000000000000..233f7a2122c7
--- /dev/null
+++ b/src/Core/LongRunning/OperationResponseTrait.php
@@ -0,0 +1,102 @@
+getLastProtoResponse();
+ if (is_null($response)) {
+ return null;
+ }
+
+ $response = $response->serialize($codec);
+
+ $result = null;
+ if ($operation->isDone()) {
+ $type = $response['metadata']['typeUrl'];
+ $result = $this->deserializeResult($operation, $type, $codec, $lroMappers);
+ }
+
+ $error = $operation->getError();
+ if (!is_null($error)) {
+ $error = $error->serialize($codec);
+ }
+
+ $response['response'] = $result;
+ $response['error'] = $error;
+
+ return $response;
+ }
+
+ /**
+ * Fetch an OperationResponse object from a gapic client.
+ *
+ * @param mixed $client A generated client with a `resumeOperation` method.
+ * @param string $name The Operation name.
+ * @return OperationResponse
+ */
+ private function getOperationByName($client, $name)
+ {
+ return $client->resumeOperation($name);
+ }
+
+ /**
+ * Convert an operation response to an array
+ *
+ * @param OperationResponse $operation The operation to serialize.
+ * @param string $type The Operation type. The type should correspond to a
+ * member of $mappers.typeUrl.
+ * @param CodecInterface $codec The gRPC codec to use for the deserialization.
+ * @param array $mappers A list of mappers.
+ * @return array|null
+ */
+ private function deserializeResult(OperationResponse $operation, $type, CodecInterface $codec, array $mappers)
+ {
+ $mappers = array_filter($mappers, function ($mapper) use ($type) {
+ return $mapper['typeUrl'] === $type;
+ });
+
+ if (count($mappers) === 0) {
+ throw new \RuntimeException(sprintf('No mapper exists for operation response type %s.', $type));
+ }
+
+ $mapper = current($mappers);
+ $message = $mapper['message'];
+
+ $response = new $message();
+ $anyResponse = $operation->getLastProtoResponse()->getResponse();
+
+ if (is_null($anyResponse)) {
+ return null;
+ }
+
+ $response->parse($anyResponse->getValue());
+
+ return $response->serialize($codec);
+ }
+}
diff --git a/src/PhpArray.php b/src/Core/PhpArray.php
similarity index 89%
rename from src/PhpArray.php
rename to src/Core/PhpArray.php
index 984061bbbe3f..e2f1c13cd436 100644
--- a/src/PhpArray.php
+++ b/src/Core/PhpArray.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
use DrSlump\Protobuf;
use google\protobuf\Struct;
@@ -33,12 +33,28 @@ class PhpArray extends Protobuf\Codec\PhpArray
private $customFilters;
/**
- * @param array $customFilters A set of callbacks to apply to properties in
+ * @var bool
+ */
+ private $useCamelCase;
+
+ /**
+ * @param array $config [optional] {
+ * Configuration Options
+ *
+ * @type array $customFilters A set of callbacks to apply to properties in
* a gRPC response.
+ * @type bool $useCamelCase Whether to convert key casing to camelCase.
+ * }
*/
- public function __construct(array $customFilters = [])
+ public function __construct(array $config = [])
{
- $this->customFilters = $customFilters;
+ $config += [
+ 'useCamelCase' => true,
+ 'customFilters' => []
+ ];
+
+ $this->customFilters = $config['customFilters'];
+ $this->useCamelCase = $config['useCamelCase'];
}
/**
@@ -96,7 +112,7 @@ protected function encodeMessage(Protobuf\Message $message)
$v = $this->filterValue($v, $field);
}
- $key = $this->toCamelCase($key);
+ $key = ($this->useCamelCase) ? $this->toCamelCase($key) : $key;
if (isset($this->customFilters[$key])) {
$v = call_user_func($this->customFilters[$key], $v);
diff --git a/src/Core/README.md b/src/Core/README.md
new file mode 100644
index 000000000000..faacd928ec81
--- /dev/null
+++ b/src/Core/README.md
@@ -0,0 +1,17 @@
+# Google Cloud PHP Core Libraries
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-core/latest/core/readme)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+**NOTE** This package is not intended for direct use. It provides common infrastructure
+to the rest of the Google Cloud PHP components.
+
+```
+$ composer require google/cloud-core
+```
diff --git a/src/RequestBuilder.php b/src/Core/RequestBuilder.php
similarity index 96%
rename from src/RequestBuilder.php
rename to src/Core/RequestBuilder.php
index f0b8064f1e0b..38116422d3f8 100644
--- a/src/RequestBuilder.php
+++ b/src/Core/RequestBuilder.php
@@ -15,9 +15,8 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
-use Google\Cloud\UriTrait;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
@@ -27,6 +26,7 @@
*/
class RequestBuilder
{
+ use JsonTrait;
use UriTrait;
/**
@@ -126,7 +126,7 @@ public function build($resource, $method, array $options = [])
$action['httpMethod'],
$uri,
['Content-Type' => 'application/json'],
- $body ? json_encode($body) : null
+ $body ? $this->jsonEncode($body) : null
);
}
@@ -136,7 +136,7 @@ public function build($resource, $method, array $options = [])
*/
private function loadServiceDefinition($servicePath)
{
- return json_decode(
+ return $this->jsonDecode(
file_get_contents($servicePath, true),
true
);
diff --git a/src/RequestWrapper.php b/src/Core/RequestWrapper.php
similarity index 76%
rename from src/RequestWrapper.php
rename to src/Core/RequestWrapper.php
index 66d3e6579aae..0cc5bece2146 100644
--- a/src/RequestWrapper.php
+++ b/src/Core/RequestWrapper.php
@@ -15,12 +15,11 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\HttpHandler\HttpHandlerFactory;
-use Google\Cloud\Exception;
-use Google\Cloud\RequestWrapperTrait;
+use Google\Cloud\Core\RequestWrapperTrait;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
@@ -32,8 +31,14 @@
*/
class RequestWrapper
{
+ use JsonTrait;
use RequestWrapperTrait;
+ /**
+ * @var string
+ */
+ private $componentVersion;
+
/**
* @var string Access token used to sign requests.
*/
@@ -53,7 +58,7 @@ class RequestWrapper
/**
* @var array HTTP client specific configuration options.
*/
- private $httpOptions;
+ private $restOptions;
/**
* @var array
@@ -80,14 +85,18 @@ class RequestWrapper
/**
* @param array $config [optional] {
* Configuration options. Please see
- * {@see Google\Cloud\RequestWrapperTrait::setCommonDefaults()} for the other
- * available options.
+ * {@see Google\Cloud\Core\RequestWrapperTrait::setCommonDefaults()} for
+ * the other available options.
*
+ * @type string $componentName The name of the component from which the request
+ * originated.
+ * @type string $componentVersion The current version of the component from
+ * which the request originated.
* @type string $accessToken Access token used to sign requests.
* @type callable $authHttpHandler A handler used to deliver Psr7
* requests specifically for authentication.
* @type callable $httpHandler A handler used to deliver Psr7 requests.
- * @type array $httpOptions HTTP client specific configuration options.
+ * @type array $restOptions HTTP client specific configuration options.
* @type bool $shouldSignRequest Whether to enable request signing.
* }
*/
@@ -98,14 +107,16 @@ public function __construct(array $config = [])
'accessToken' => null,
'authHttpHandler' => null,
'httpHandler' => null,
- 'httpOptions' => [],
- 'shouldSignRequest' => true
+ 'restOptions' => [],
+ 'shouldSignRequest' => true,
+ 'componentVersion' => null
];
+ $this->componentVersion = $config['componentVersion'];
$this->accessToken = $config['accessToken'];
$this->httpHandler = $config['httpHandler'] ?: HttpHandlerFactory::build();
$this->authHttpHandler = $config['authHttpHandler'] ?: $this->httpHandler;
- $this->httpOptions = $config['httpOptions'];
+ $this->restOptions = $config['restOptions'];
$this->shouldSignRequest = $config['shouldSignRequest'];
}
@@ -116,40 +127,49 @@ public function __construct(array $config = [])
* @param array $options [optional] {
* Request options.
*
+ * @type float $requestTimeout Seconds to wait before timing out the
+ * request. **Defaults to** `0`.
* @type int $retries Number of retries for a failed request.
* **Defaults to** `3`.
- * @type array $httpOptions HTTP client specific configuration options.
+ * @type array $restOptions HTTP client specific configuration options.
* }
* @return ResponseInterface
*/
public function send(RequestInterface $request, array $options = [])
{
$retries = isset($options['retries']) ? $options['retries'] : $this->retries;
- $httpOptions = isset($options['httpOptions']) ? $options['httpOptions'] : $this->httpOptions;
+ $restOptions = isset($options['restOptions']) ? $options['restOptions'] : $this->restOptions;
+ $timeout = isset($options['requestTimeout']) ? $options['requestTimeout'] : $this->requestTimeout;
$backoff = new ExponentialBackoff($retries, $this->getRetryFunction());
- $signedRequest = $this->shouldSignRequest ? $this->signRequest($request) : $request;
+ if ($timeout && !array_key_exists('timeout', $restOptions)) {
+ $restOptions['timeout'] = $timeout;
+ }
try {
- return $backoff->execute($this->httpHandler, [$signedRequest, $httpOptions]);
+ return $backoff->execute($this->httpHandler, [$this->applyHeaders($request), $restOptions]);
} catch (\Exception $ex) {
throw $this->convertToGoogleException($ex);
}
}
/**
- * Sign the request.
+ * Applies headers to the request.
*
* @param RequestInterface $request Psr7 request.
* @return RequestInterface
*/
- private function signRequest(RequestInterface $request)
+ private function applyHeaders(RequestInterface $request)
{
$headers = [
- 'User-Agent' => 'gcloud-php/' . ServiceBuilder::VERSION,
- 'Authorization' => 'Bearer ' . $this->getToken()
+ 'User-Agent' => 'gcloud-php/' . $this->componentVersion,
+ 'x-goog-api-client' => 'gl-php/' . phpversion() . ' gccl/' . $this->componentVersion,
];
+ if ($this->shouldSignRequest) {
+ $headers['Authorization'] = 'Bearer ' . $this->getToken();
+ }
+
return Psr7\modify_request($request, ['set_headers' => $headers]);
}
@@ -207,6 +227,10 @@ private function convertToGoogleException(\Exception $ex)
$exception = Exception\ConflictException::class;
break;
+ case 412:
+ $exception = Exception\FailedPreconditionException::class;
+ break;
+
case 500:
$exception = Exception\ServerException::class;
break;
@@ -229,9 +253,12 @@ private function getExceptionMessage(\Exception $ex)
{
if ($ex instanceof RequestException && $ex->hasResponse()) {
$res = (string) $ex->getResponse()->getBody();
- json_decode($res);
- if (json_last_error() === JSON_ERROR_NONE) {
+
+ try {
+ $this->jsonDecode($res);
return $res;
+ } catch (\InvalidArgumentException $e) {
+ // no-op
}
}
diff --git a/src/RequestWrapperTrait.php b/src/Core/RequestWrapperTrait.php
similarity index 91%
rename from src/RequestWrapperTrait.php
rename to src/Core/RequestWrapperTrait.php
index 90bea8b03550..2d4830ab6543 100644
--- a/src/RequestWrapperTrait.php
+++ b/src/Core/RequestWrapperTrait.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
use Google\Auth\ApplicationDefaultCredentials;
use Google\Auth\Cache\MemoryCacheItemPool;
@@ -50,6 +50,12 @@ trait RequestWrapperTrait
*/
private $keyFile;
+ /**
+ * @var float Seconds to wait before timing out the request. **Defaults to**
+ * `0` with REST and `60` with gRPC.
+ */
+ private $requestTimeout;
+
/**
* @var int Number of retries for a failed request. **Defaults to** `3`.
*/
@@ -74,6 +80,8 @@ trait RequestWrapperTrait
* @type array $keyFile The contents of the service account credentials
* .json file retrieved from the Google Developer's Console.
* Ex: `json_decode(file_get_contents($path), true)`.
+ * @type float $requestTimeout Seconds to wait before timing out the
+ * request. **Defaults to** `0` with REST and `60` with gRPC.
* @type int $retries Number of retries for a failed request.
* **Defaults to** `3`.
* @type array $scopes Scopes to be used for the request.
@@ -87,6 +95,7 @@ public function setCommonDefaults(array $config)
'authCacheOptions' => [],
'credentialsFetcher' => null,
'keyFile' => null,
+ 'requestTimeout' => null,
'retries' => null,
'scopes' => null
];
diff --git a/src/RestTrait.php b/src/Core/RestTrait.php
similarity index 90%
rename from src/RestTrait.php
rename to src/Core/RestTrait.php
index 8a451d0ca395..663687a807e9 100644
--- a/src/RestTrait.php
+++ b/src/Core/RestTrait.php
@@ -15,16 +15,16 @@
* limitations under the License.
*/
-namespace Google\Cloud;
-
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
+namespace Google\Cloud\Core;
/**
* Provides shared functionality for REST service implementations.
*/
trait RestTrait
{
+ use ArrayTrait;
+ use JsonTrait;
+
/**
* @var RequestBuilder Builds PSR7 requests from a service definition.
*/
@@ -68,10 +68,11 @@ public function setRequestWrapper(RequestWrapper $requestWrapper)
*/
public function send($resource, $method, array $options = [])
{
- $requestOptions = array_intersect_key($options, [
- 'httpOptions' => null,
- 'retries' => null
- ]);
+ $requestOptions = $this->pluckArray([
+ 'restOptions',
+ 'retries',
+ 'requestTimeout'
+ ], $options);
return json_decode(
$this->requestWrapper->send(
diff --git a/src/Core/Retry.php b/src/Core/Retry.php
new file mode 100644
index 000000000000..feb118eb4053
--- /dev/null
+++ b/src/Core/Retry.php
@@ -0,0 +1,106 @@
+ (int >= 0), 'nanos' => (int >= 0)] specifying how
+ * long an operation should pause before retrying. Should accept a
+ * single argument of type `\Exception`.
+ * @param callable $retryFunction [optional] returns bool for whether or not
+ * to retry.
+ */
+ public function __construct(
+ $retries,
+ callable $delayFunction,
+ callable $retryFunction = null
+ ) {
+ $this->retries = $retries !== null ? (int) $retries : 3;
+ $this->retryFunction = $retryFunction;
+ $this->delayFunction = $delayFunction;
+ }
+
+ /**
+ * Executes the retry process.
+ *
+ * @param callable $function
+ * @param array $arguments [optional]
+ * @return mixed
+ * @throws \Exception The last exception caught while retrying.
+ */
+ public function execute(callable $function, array $arguments = [])
+ {
+ $delayFunction = $this->delayFunction;
+ $retryAttempt = 0;
+ $exception = null;
+
+ while (true) {
+ try {
+ return call_user_func_array($function, $arguments);
+ } catch (\Exception $exception) {
+ if ($this->retryFunction) {
+ if (!call_user_func($this->retryFunction, $exception)) {
+ throw $exception;
+ }
+ }
+
+ if ($retryAttempt >= $this->retries) {
+ break;
+ }
+
+ $delayFunction($exception);
+ $retryAttempt++;
+ }
+ }
+
+ throw $exception;
+ }
+
+ /**
+ * @param callable $delayFunction
+ * @return void
+ */
+ public function setDelayFunction(callable $delayFunction)
+ {
+ $this->delayFunction = $delayFunction;
+ }
+}
diff --git a/src/Upload/AbstractUploader.php b/src/Core/Upload/AbstractUploader.php
similarity index 87%
rename from src/Upload/AbstractUploader.php
rename to src/Core/Upload/AbstractUploader.php
index cb5fb73cd12e..ca8ce62479ee 100644
--- a/src/Upload/AbstractUploader.php
+++ b/src/Core/Upload/AbstractUploader.php
@@ -15,10 +15,10 @@
* limitations under the License.
*/
-namespace Google\Cloud\Upload;
+namespace Google\Cloud\Core\Upload;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\UriTrait;
use GuzzleHttp\Psr7;
use Psr\Http\Message\StreamInterface;
@@ -78,7 +78,9 @@ abstract class AbstractUploader
* @type array $metadata Metadata on the resource.
* @type int $chunkSize Size of the chunks to send incrementally during
* a resumable upload. Must be in multiples of 262144 bytes.
- * @type array $httpOptions HTTP client specific configuration options.
+ * @type array $restOptions HTTP client specific configuration options.
+ * @type float $requestTimeout Seconds to wait before timing out the
+ * request. **Defaults to** `0`.
* @type int $retries Number of retries for a failed request.
* **Defaults to** `3`.
* @type string $contentType Content type of the resource.
@@ -96,8 +98,9 @@ public function __construct(
$this->metadata = isset($options['metadata']) ? $options['metadata'] : [];
$this->chunkSize = isset($options['chunkSize']) ? $options['chunkSize'] : null;
$this->requestOptions = array_intersect_key($options, [
- 'httpOptions' => null,
- 'retries' => null
+ 'restOptions' => null,
+ 'retries' => null,
+ 'requestTimeout' => null
]);
$this->contentType = isset($options['contentType'])
diff --git a/src/Upload/MultipartUploader.php b/src/Core/Upload/MultipartUploader.php
similarity index 90%
rename from src/Upload/MultipartUploader.php
rename to src/Core/Upload/MultipartUploader.php
index 90b01471bcc0..34251a70c71f 100644
--- a/src/Upload/MultipartUploader.php
+++ b/src/Core/Upload/MultipartUploader.php
@@ -15,8 +15,9 @@
* limitations under the License.
*/
-namespace Google\Cloud\Upload;
+namespace Google\Cloud\Core\Upload;
+use Google\Cloud\Core\JsonTrait;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
@@ -25,6 +26,8 @@
*/
class MultipartUploader extends AbstractUploader
{
+ use JsonTrait;
+
/**
* Triggers the upload process.
*
@@ -36,7 +39,7 @@ public function upload()
[
'name' => 'metadata',
'headers' => ['Content-Type' => 'application/json; charset=UTF-8'],
- 'contents' => json_encode($this->metadata)
+ 'contents' => $this->jsonEncode($this->metadata)
],
[
'name' => 'data',
@@ -50,7 +53,7 @@ public function upload()
'Content-Length' => $multipartStream->getSize()
];
- return json_decode(
+ return $this->jsonDecode(
$this->requestWrapper->send(
new Request(
'POST',
diff --git a/src/Upload/ResumableUploader.php b/src/Core/Upload/ResumableUploader.php
similarity index 92%
rename from src/Upload/ResumableUploader.php
rename to src/Core/Upload/ResumableUploader.php
index dea4dcb44e59..3d099cff04c6 100644
--- a/src/Upload/ResumableUploader.php
+++ b/src/Core/Upload/ResumableUploader.php
@@ -15,9 +15,10 @@
* limitations under the License.
*/
-namespace Google\Cloud\Upload;
+namespace Google\Cloud\Core\Upload;
-use Google\Cloud\Exception\GoogleException;
+use Google\Cloud\Core\Exception\GoogleException;
+use Google\Cloud\Core\JsonTrait;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LimitStream;
use GuzzleHttp\Psr7\Request;
@@ -28,10 +29,12 @@
*/
class ResumableUploader extends AbstractUploader
{
+ use JsonTrait;
+
/**
* @var int
*/
- private $rangeStart = 0;
+ protected $rangeStart = 0;
/**
* @var string
@@ -69,7 +72,7 @@ public function resume($resumeUri)
$response = $this->getStatusResponse();
if ($response->getBody()->getSize() > 0) {
- return json_decode($response->getBody(), true);
+ return $this->jsonDecode($response->getBody(), true);
}
$this->rangeStart = $this->getRangeStart($response->getHeaderLine('Range'));
@@ -122,7 +125,7 @@ public function upload()
$rangeStart = $this->getRangeStart($response->getHeaderLine('Range'));
} while ($response->getStatusCode() === 308);
- return json_decode($response->getBody(), true);
+ return $this->jsonDecode($response->getBody(), true);
}
/**
@@ -142,7 +145,7 @@ private function createResumeUri()
'POST',
$this->uri,
$headers,
- json_encode($this->metadata)
+ $this->jsonEncode($this->metadata)
);
$response = $this->requestWrapper->send($request, $this->requestOptions);
diff --git a/src/Core/Upload/StreamableUploader.php b/src/Core/Upload/StreamableUploader.php
new file mode 100644
index 000000000000..3d876dc9502e
--- /dev/null
+++ b/src/Core/Upload/StreamableUploader.php
@@ -0,0 +1,85 @@
+getResumeUri();
+
+ if ($writeSize) {
+ $rangeEnd = $this->rangeStart + $writeSize - 1;
+ $data = $this->data->read($writeSize);
+ } else {
+ $rangeEnd = '*';
+ $data = $this->data;
+ }
+
+ // do the streaming write
+ $headers = [
+ 'Content-Length' => $writeSize,
+ 'Content-Type' => $this->contentType,
+ 'Content-Range' => "bytes {$this->rangeStart}-$rangeEnd/*"
+ ];
+
+ $request = new Request(
+ 'PUT',
+ $resumeUri,
+ $headers,
+ $data
+ );
+
+ try {
+ $response = $this->requestWrapper->send($request, $this->requestOptions);
+ } catch (ServiceException $ex) {
+ throw new GoogleException(
+ "Upload failed. Please use this URI to resume your upload: $resumeUri",
+ $ex->getCode(),
+ $ex
+ );
+ }
+
+ // reset the buffer with the remaining contents
+ $this->rangeStart += $writeSize;
+
+ return json_decode($response->getBody(), true);
+ }
+}
diff --git a/src/UriTrait.php b/src/Core/UriTrait.php
similarity index 98%
rename from src/UriTrait.php
rename to src/Core/UriTrait.php
index 6e0313488c5d..8b1df091ce06 100644
--- a/src/UriTrait.php
+++ b/src/Core/UriTrait.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
use GuzzleHttp\Psr7;
use Psr\Http\Message\UriInterface;
diff --git a/src/Core/VERSION b/src/Core/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/Core/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/ValidateTrait.php b/src/Core/ValidateTrait.php
similarity index 98%
rename from src/ValidateTrait.php
rename to src/Core/ValidateTrait.php
index a13c7e106e47..52c7343f4bec 100644
--- a/src/ValidateTrait.php
+++ b/src/Core/ValidateTrait.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud;
+namespace Google\Cloud\Core;
use InvalidArgumentException;
diff --git a/src/Core/composer.json b/src/Core/composer.json
new file mode 100644
index 000000000000..cee03e142b4a
--- /dev/null
+++ b/src/Core/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "google/cloud-core",
+ "description": "Google Cloud PHP shared dependency, providing functionality useful to all components.",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "php": ">=5.5",
+ "rize/uri-template": "~0.3",
+ "google/auth": "^0.11",
+ "guzzlehttp/guzzle": "^5.3|^6.0",
+ "guzzlehttp/psr7": "^1.2",
+ "monolog/monolog": "~1",
+ "psr/http-message": "1.0.*"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-core",
+ "target": "GoogleCloudPlatform/google-cloud-php-core.git",
+ "path": "src/Core",
+ "entry": null
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Core\\": ""
+ }
+ }
+}
diff --git a/src/Datastore/Blob.php b/src/Datastore/Blob.php
index 2af20fbeebb5..ceecf8b5cdb6 100644
--- a/src/Datastore/Blob.php
+++ b/src/Datastore/Blob.php
@@ -27,10 +27,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
+ * $datastore = new DatastoreClient();
*
* $blob = $datastore->blob(file_get_contents(__DIR__ .'/family-photo.jpg'));
* ```
diff --git a/src/Datastore/Connection/Rest.php b/src/Datastore/Connection/Rest.php
index 1b6e32cebace..0c0f54857976 100644
--- a/src/Datastore/Connection/Rest.php
+++ b/src/Datastore/Connection/Rest.php
@@ -17,11 +17,12 @@
namespace Google\Cloud\Datastore\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\EmulatorTrait;
-use Google\Cloud\RestTrait;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\EmulatorTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\UriTrait;
+use Google\Cloud\Datastore\DatastoreClient;
/**
* Implementation of the
@@ -45,7 +46,8 @@ public function __construct(array $config = [])
$baseUri = $this->getEmulatorBaseUri(self::BASE_URI, $emulatorHost);
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/datastore-v1.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/datastore-v1.json',
+ 'componentVersion' => DatastoreClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
diff --git a/src/Datastore/DatastoreClient.php b/src/Datastore/DatastoreClient.php
index bab400e6a48f..477d8a2c1f7f 100644
--- a/src/Datastore/DatastoreClient.php
+++ b/src/Datastore/DatastoreClient.php
@@ -18,42 +18,34 @@
namespace Google\Cloud\Datastore;
use DomainException;
-use Google\Cloud\ClientTrait;
+use Google\Cloud\Core\ClientTrait;
use Google\Cloud\Datastore\Connection\Rest;
use Google\Cloud\Datastore\Query\GqlQuery;
use Google\Cloud\Datastore\Query\Query;
use Google\Cloud\Datastore\Query\QueryBuilder;
use Google\Cloud\Datastore\Query\QueryInterface;
-use Google\Cloud\Int64;
+use Google\Cloud\Core\Int64;
use InvalidArgumentException;
use Psr\Cache\CacheItemPoolInterface;
+use Psr\Http\Message\StreamInterface;
/**
- * Google Cloud Datastore client. Cloud Datastore is a highly-scalable NoSQL
- * database for your applications. Find more information at
+ * Google Cloud Datastore is a highly-scalable NoSQL database for your
+ * applications. Find more information at the
* [Google Cloud Datastore docs](https://cloud.google.com/datastore/docs/).
*
- * Cloud Datastore supports [multi-tenant](https://cloud.google.com/datastore/docs/concepts/multitenancy) applications
- * through use of data partitions. A partition ID can be supplied when creating an instance of Cloud Datastore, and will
- * be used in all operations executed in that instance.
+ * Cloud Datastore supports
+ * [multi-tenant](https://cloud.google.com/datastore/docs/concepts/multitenancy)
+ * applications through use of data partitions. A partition ID can be supplied
+ * when creating an instance of Cloud Datastore, and will be used in all
+ * operations executed in that instance.
*
* To enable the
* [Google Cloud Datastore Emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator),
- * set the
- * [`PUBSUB_EMULATOR_HOST`](https://cloud.google.com/datastore/docs/tools/datastore-emulator#setting_environment_variables)
- * environment variable.
+ * set the [`DATASTORE_EMULATOR_HOST`](https://goo.gl/vCVZrY) environment variable.
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- *
- * $datastore = $cloud->datastore();
- * ```
- *
- * ```
- * // DatastoreClient can be instantiated directly.
* use Google\Cloud\Datastore\DatastoreClient;
*
* $datastore = new DatastoreClient();
@@ -61,25 +53,22 @@
*
* ```
* // Multi-tenant applications can supply a namespace ID.
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $datastore = $cloud->datastore([
+ * $datastore = new DatastoreClient([
* 'namespaceId' => 'my-application-namespace'
* ]);
* ```
*
* ```
* // Using the Datastore Emulator
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
* // Be sure to use the port specified when starting the emulator.
* // `8900` is used as an example only.
* putenv('DATASTORE_EMULATOR_HOST=http://localhost:8900');
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
+ * $datastore = new DatastoreClient();
* ```
*/
class DatastoreClient
@@ -87,6 +76,8 @@ class DatastoreClient
use ClientTrait;
use DatastoreTrait;
+ const VERSION = '0.1.0';
+
const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/datastore';
/**
@@ -131,7 +122,7 @@ class DatastoreClient
* @type string $namespaceId Partitions data under a namespace. Useful for
* [Multitenant Projects](https://cloud.google.com/datastore/docs/concepts/multitenancy).
* @type bool $returnInt64AsObject If true, 64 bit integers will be
- * returned as a {@see Google\Cloud\Int64} object for 32 bit
+ * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
* }
* @throws \InvalidArgumentException
@@ -372,7 +363,7 @@ public function geoPoint($latitude, $longitude)
* $blob = $datastore->blob(file_get_contents(__DIR__ .'/family-photo.jpg'));
* ```
*
- * @param string|resource|StreamInterface $value
+ * @param string|resource|StreamInterface $value The value to store in a blob.
* @return Blob
*/
public function blob($value)
@@ -494,6 +485,8 @@ public function transaction(array $options = [])
* $datastore->insert($entity);
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param Entity $entity The entity to be inserted.
* @param array $options [optional] Configuration options.
* @return string The entity version.
@@ -524,6 +517,8 @@ public function insert(Entity $entity, array $options = [])
* $datastore->insertBatch($entities);
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param Entity[] $entities The entities to be inserted.
* @param array $options [optional] Configuration options.
* @return array [Response Body](https://cloud.google.com/datastore/reference/rest/v1/projects/commit#response-body)
@@ -556,6 +551,8 @@ public function insertBatch(array $entities, array $options = [])
* $datastore->update($entity);
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param Entity $entity The entity to be updated.
* @param array $options [optional] {
* Configuration Options
@@ -596,6 +593,8 @@ public function update(Entity $entity, array $options = [])
* $datastore->updateBatch($entities);
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param Entity[] $entities The entities to be updated.
* @param array $options [optional] {
* Configuration Options
@@ -648,6 +647,8 @@ public function updateBatch(array $entities, array $options = [])
* $datastore->upsert($entity);
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param Entity $entity The entity to be upserted.
* @param array $options [optional] Configuration Options.
* @return string The entity version.
@@ -688,6 +689,8 @@ public function upsert(Entity $entity, array $options = [])
* $datastore->upsertBatch($entities);
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param Entity[] $entities The entities to be upserted.
* @param array $options [optional] Configuration Options.
* @return array [Response Body](https://cloud.google.com/datastore/reference/rest/v1/projects/commit#response-body)
@@ -715,6 +718,8 @@ public function upsertBatch(array $entities, array $options = [])
* $datastore->delete($key);
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param Key $key The identifier to delete.
* @param array $options [optional] {
* Configuration options
@@ -749,6 +754,8 @@ public function delete(Key $key, array $options = [])
* $datastore->deleteBatch($keys);
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param Key[] $keys The identifiers to delete.
* @param array $options [optional] {
* Configuration options
@@ -790,6 +797,8 @@ public function deleteBatch(array $keys, array $options = [])
* }
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/lookup Lookup API documentation
+ *
* @param Key $key The identifier to use to locate a desired entity.
* @param array $options [optional] {
* Configuration Options
@@ -831,6 +840,8 @@ public function lookup(Key $key, array $options = [])
* }
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/lookup Lookup API documentation
+ *
* @param Key[] $key The identifiers to look up.
* @param array $options [optional] {
* Configuration Options
@@ -949,6 +960,8 @@ public function gqlQuery($query, array $options = [])
* }
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/runQuery RunQuery API documentation
+ *
* @param QueryInterface $query A query object.
* @param array $options [optional] {
* Configuration Options
@@ -959,7 +972,7 @@ public function gqlQuery($query, array $options = [])
* @type string $readConsistency See
* [ReadConsistency](https://cloud.google.com/datastore/reference/rest/v1/ReadOptions#ReadConsistency).
* }
- * @return \Generator
+ * @return EntityIterator
*/
public function runQuery(QueryInterface $query, array $options = [])
{
diff --git a/src/Datastore/DatastoreSessionHandler.php b/src/Datastore/DatastoreSessionHandler.php
index e4248e27708f..d1793db25ce4 100644
--- a/src/Datastore/DatastoreSessionHandler.php
+++ b/src/Datastore/DatastoreSessionHandler.php
@@ -59,11 +59,9 @@
*
* Example without error handling:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
- * // or just $datastore = new \Google\Cloud\Datastore\DatastoreClient();
+ * $datastore = new DatastoreClient();
*
* $handler = new DatastoreSessionHandler($datastore);
*
@@ -85,11 +83,9 @@
* Example with error handling:
*
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
- * // or just $datastore = new \Google\Cloud\Datastore\DatastoreClient();
+ * $datastore = new DatastoreClient;
*
* $handler = new DatastoreSessionHandler($datastore);
* session_set_save_handler($handler, true);
diff --git a/src/Datastore/Entity.php b/src/Datastore/Entity.php
index bdc712745168..56bd62db5018 100644
--- a/src/Datastore/Entity.php
+++ b/src/Datastore/Entity.php
@@ -37,7 +37,7 @@
* | {@see Google\Cloud\Datastore\GeoPoint} | `geoPointValue` |
* | {@see Google\Cloud\Datastore\Entity} | `entityValue` |
* | {@see Google\Cloud\Datastore\Blob} | `blobValue` |
- * | {@see Google\Cloud\Int64} | `integerValue` |
+ * | {@see Google\Cloud\Core\Int64} | `integerValue` |
* | Associative Array | `entityValue` (No Key) |
* | Non-Associative Array | `arrayValue` |
* | `float` | `doubleValue` |
@@ -50,10 +50,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
+ * $datastore = new DatastoreClient();
*
* $key = $datastore->key('Person', 'Bob');
* $entity = $datastore->entity($key, [
@@ -64,6 +63,8 @@
* echo $entity['firstName']; // 'Bob'
* $entity['location'] = 'Detroit, MI';
* ```
+ *
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/Entity Entity API documentation
*/
class Entity implements ArrayAccess
{
@@ -179,6 +180,8 @@ public function key()
* $cursor = $entity->cursor();
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/EntityResult EntityResult.cursor
+ *
* @return string|null
*/
public function cursor()
@@ -197,6 +200,8 @@ public function cursor()
* $baseVersion = $entity->baseVersion();
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/EntityResult EntitResult.version
+ *
* @return string|null
*/
public function baseVersion()
diff --git a/src/Datastore/EntityIterator.php b/src/Datastore/EntityIterator.php
new file mode 100644
index 000000000000..94cd58810afa
--- /dev/null
+++ b/src/Datastore/EntityIterator.php
@@ -0,0 +1,42 @@
+pageIterator->moreResultsType();
+ }
+}
diff --git a/src/Datastore/EntityMapper.php b/src/Datastore/EntityMapper.php
index 83c97b0b6cb7..50e9b516d4aa 100644
--- a/src/Datastore/EntityMapper.php
+++ b/src/Datastore/EntityMapper.php
@@ -17,11 +17,11 @@
namespace Google\Cloud\Datastore;
-use Google\Cloud\ArrayTrait;
+use Google\Cloud\Core\ArrayTrait;
use Google\Cloud\Datastore\Entity;
use Google\Cloud\Datastore\GeoPoint;
use Google\Cloud\Datastore\Key;
-use Google\Cloud\Int64;
+use Google\Cloud\Core\Int64;
use InvalidArgumentException;
use RuntimeException;
@@ -57,7 +57,7 @@ class EntityMapper
* @param string $projectId The datastore project ID
* @param bool $encode Whether to encode blobs as base64.
* @param bool $returnInt64AsObject If true, 64 bit integers will be
- * returned as a {@see Google\Cloud\Int64} object for 32 bit
+ * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility.
*/
public function __construct($projectId, $encode, $returnInt64AsObject)
diff --git a/src/Datastore/EntityPageIterator.php b/src/Datastore/EntityPageIterator.php
new file mode 100644
index 000000000000..ea590ed3b574
--- /dev/null
+++ b/src/Datastore/EntityPageIterator.php
@@ -0,0 +1,66 @@
+moreResultsType;
+ }
+
+ /**
+ * Get the current page.
+ *
+ * @return array|null
+ */
+ public function current()
+ {
+ if (!$this->page) {
+ $this->page = $this->executeCall();
+ }
+
+ $this->moreResultsType = isset($this->page['batch']['moreResults'])
+ ? $this->page['batch']['moreResults']
+ : null;
+
+ return $this->get($this->itemsPath, $this->page);
+ }
+}
diff --git a/src/Datastore/GeoPoint.php b/src/Datastore/GeoPoint.php
index ac03a29c1cfe..399988cce189 100644
--- a/src/Datastore/GeoPoint.php
+++ b/src/Datastore/GeoPoint.php
@@ -24,7 +24,11 @@
*
* Example:
* ```
- * $point = new GeoPoint(37.423147, -122.085015);
+ * use Google\Cloud\Datastore\DatastoreClient;
+ *
+ * $datastore = new DatastoreClient();
+ *
+ * $point = $datastore->geoPoint(37.423147, -122.085015);
* ```
*
* @see https://cloud.google.com/datastore/reference/rest/Shared.Types/LatLng LatLng
diff --git a/src/Datastore/Key.php b/src/Datastore/Key.php
index 8e16d1bb7fcb..bfba833f90a4 100644
--- a/src/Datastore/Key.php
+++ b/src/Datastore/Key.php
@@ -17,13 +17,11 @@
namespace Google\Cloud\Datastore;
-use Google\Cloud\ArrayTrait;
+use Google\Cloud\Core\ArrayTrait;
use InvalidArgumentException;
use JsonSerializable;
/**
- * Represents a Datastore Key.
- *
* Keys are unique identifiers for entities.
*
* Keys may be considered either "named" or "incomplete". A named Key is one in
@@ -46,10 +44,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
+ * $datastore = new DatastoreClient();
*
* $key = $datastore->key('Person', 'Bob');
* ```
diff --git a/src/Datastore/LICENSE b/src/Datastore/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/Datastore/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/Datastore/Operation.php b/src/Datastore/Operation.php
index 8372872b3cb5..9a74141349cb 100644
--- a/src/Datastore/Operation.php
+++ b/src/Datastore/Operation.php
@@ -17,10 +17,10 @@
namespace Google\Cloud\Datastore;
+use Google\Cloud\Core\ValidateTrait;
use Google\Cloud\Datastore\Connection\ConnectionInterface;
use Google\Cloud\Datastore\Connection\Rest;
use Google\Cloud\Datastore\Query\QueryInterface;
-use Google\Cloud\ValidateTrait;
use InvalidArgumentException;
/**
@@ -328,10 +328,9 @@ public function lookup(array $keys, array $options = [])
$result = [];
if (isset($res['found'])) {
- $result['found'] = $this->mapEntityResult(
- $res['found'],
- $options['className']
- );
+ foreach ($res['found'] as $found) {
+ $result['found'][] = $this->mapEntityResult($found, $options['className']);
+ }
if ($options['sort']) {
$result['found'] = $this->sortEntities($result['found'], $keys);
@@ -380,7 +379,7 @@ public function lookup(array $keys, array $options = [])
* @type string $readConsistency See
* [ReadConsistency](https://cloud.google.com/datastore/reference/rest/v1/ReadOptions#ReadConsistency).
* }
- * @return \Generator
+ * @return EntityIterator
*/
public function runQuery(QueryInterface $query, array $options = [])
{
@@ -388,36 +387,34 @@ public function runQuery(QueryInterface $query, array $options = [])
'className' => null,
'namespaceId' => $this->namespaceId
];
-
- $moreResults = true;
- do {
- $request = $options + $this->readOptions($options) + [
- 'projectId' => $this->projectId,
- 'partitionId' => $this->partitionId($this->projectId, $options['namespaceId']),
- $query->queryKey() => $query->queryObject()
- ];
-
- $res = $this->connection->runQuery($request);
-
- if (isset($res['batch']['entityResults']) && is_array($res['batch']['entityResults'])) {
- $results = $this->mapEntityResult(
- $res['batch']['entityResults'],
- $options['className']
- );
-
- foreach ($results as $result) {
- yield $result;
+ $request = $options + $this->readOptions($options) + [
+ 'projectId' => $this->projectId,
+ 'partitionId' => $this->partitionId($this->projectId, $options['namespaceId']),
+ $query->queryKey() => $query->queryObject()
+ ];
+ $iteratorConfig = [
+ 'itemsKey' => 'batch.entityResults',
+ 'resultTokenKey' => 'query.startCursor',
+ 'nextResultTokenKey' => 'batch.endCursor',
+ 'setNextResultTokenCondition' => function ($res) use ($query) {
+ if (isset($res['batch']['moreResults'])) {
+ return $query->canPaginate() && $res['batch']['moreResults'] === 'NOT_FINISHED';
}
- if ($query->canPaginate() && $res['batch']['moreResults'] === 'NOT_FINISHED') {
- $query->start($res['batch']['endCursor']);
- } else {
- $moreResults = false;
- }
- } else {
- $moreResults = false;
+ return false;
}
- } while ($moreResults);
+ ];
+
+ return new EntityIterator(
+ new EntityPageIterator(
+ function (array $entityResult) use ($options) {
+ return $this->mapEntityResult($entityResult, $options['className']);
+ },
+ [$this->connection, 'runQuery'],
+ $request,
+ $iteratorConfig
+ )
+ );
}
/**
@@ -572,7 +569,7 @@ public function checkOverwrite(array $entities, $allowOverwrite = false)
*
* @see https://cloud.google.com/datastore/reference/rest/v1/EntityResult EntityResult
*
- * @param array $entityResult The EntityResult from a Lookup.
+ * @param array $result The EntityResult from a Lookup.
* @param string|array $class If a string, the name of the class to return results as.
* Must be a subclass of {@see Google\Cloud\Datastore\Entity}.
* If not set, {@see Google\Cloud\Datastore\Entity} will be used.
@@ -581,63 +578,57 @@ public function checkOverwrite(array $entities, $allowOverwrite = false)
* {@see Google\Cloud\Datastore\Entity}.
* @return Entity[]
*/
- private function mapEntityResult(array $entityResult, $class)
+ private function mapEntityResult(array $result, $class)
{
- $entities = [];
+ $entity = $result['entity'];
- foreach ($entityResult as $result) {
- $entity = $result['entity'];
+ $properties = [];
+ $excludes = [];
+ $meanings = [];
- $properties = [];
- $excludes = [];
- $meanings = [];
+ if (isset($entity['properties'])) {
+ $res = $this->entityMapper->responseToEntityProperties($entity['properties']);
- if (isset($entity['properties'])) {
- $res = $this->entityMapper->responseToEntityProperties($entity['properties']);
+ $properties = $res['properties'];
+ $excludes = $res['excludes'];
+ $meanings = $res['meanings'];
+ }
- $properties = $res['properties'];
- $excludes = $res['excludes'];
- $meanings = $res['meanings'];
- }
+ $namespaceId = (isset($entity['key']['partitionId']['namespaceId']))
+ ? $entity['key']['partitionId']['namespaceId']
+ : null;
- $namespaceId = (isset($entity['key']['partitionId']['namespaceId']))
- ? $entity['key']['partitionId']['namespaceId']
- : null;
-
- $key = new Key($this->projectId, [
- 'path' => $entity['key']['path'],
- 'namespaceId' => $namespaceId
- ]);
-
- if (is_array($class)) {
- $lastPathElement = $key->pathEnd();
- if (!array_key_exists($lastPathElement['kind'], $class)) {
- throw new InvalidArgumentException(sprintf(
- 'No class found for kind %s',
- $lastPathElement['kind']
- ));
- }
+ $key = new Key($this->projectId, [
+ 'path' => $entity['key']['path'],
+ 'namespaceId' => $namespaceId
+ ]);
- $className = $class[$lastPathElement['kind']];
- } else {
- $className = $class;
+ if (is_array($class)) {
+ $lastPathElement = $key->pathEnd();
+ if (!array_key_exists($lastPathElement['kind'], $class)) {
+ throw new InvalidArgumentException(sprintf(
+ 'No class found for kind %s',
+ $lastPathElement['kind']
+ ));
}
- $entities[] = $this->entity($key, $properties, [
- 'cursor' => (isset($result['cursor']))
- ? $result['cursor']
- : null,
- 'baseVersion' => (isset($result['version']))
- ? $result['version']
- : null,
- 'className' => $className,
- 'populatedByService' => true,
- 'excludeFromIndexes' => $excludes,
- 'meanings' => $meanings
- ]);
+ $className = $class[$lastPathElement['kind']];
+ } else {
+ $className = $class;
}
- return $entities;
+ return $this->entity($key, $properties, [
+ 'cursor' => (isset($result['cursor']))
+ ? $result['cursor']
+ : null,
+ 'baseVersion' => (isset($result['version']))
+ ? $result['version']
+ : null,
+ 'className' => $className,
+ 'populatedByService' => true,
+ 'excludeFromIndexes' => $excludes,
+ 'meanings' => $meanings
+ ]);
}
/**
diff --git a/src/Datastore/Query/GqlQuery.php b/src/Datastore/Query/GqlQuery.php
index 646e47b22a24..f85ed82e3546 100644
--- a/src/Datastore/Query/GqlQuery.php
+++ b/src/Datastore/Query/GqlQuery.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Datastore\Query;
-use Google\Cloud\ArrayTrait;
+use Google\Cloud\Core\ArrayTrait;
use Google\Cloud\Datastore\DatastoreTrait;
use Google\Cloud\Datastore\EntityMapper;
use InvalidArgumentException;
@@ -27,7 +27,7 @@
*
* By default, parameters MUST be bound using named or positional bindings.
* Literals are disabled by default, and must be enabled by setting
- * `$options['allowLiterals']` to `true`. As with any SQL-style language, using
+ * `$options['allowLiterals']` to `true`. As with any SQL dialect, using
* parameter binding is highly recommended.
*
* Idiomatic usage is via {@see Google\Cloud\Datastore\DatastoreClient::gqlQuery()}.
@@ -35,10 +35,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
+ * $datastore = new DatastoreClient();
*
* $query = $datastore->gqlQuery('SELECT * FROM Companies WHERE companyName = @companyName', [
* 'bindings' => [
diff --git a/src/Datastore/Query/Query.php b/src/Datastore/Query/Query.php
index dbc3e3f6b7e3..8b7fe70ebc7a 100644
--- a/src/Datastore/Query/Query.php
+++ b/src/Datastore/Query/Query.php
@@ -31,10 +31,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
+ * $datastore = new DatastoreClient();
*
* $query = $datastore->query();
* $query->kind('Companies');
@@ -132,9 +131,10 @@ class Query implements QueryInterface
private $query;
/**
+ * @codingStandardsIgnoreStart
* @param EntityMapper $entityMapper An instance of EntityMapper
- * @param array $query [optional]
- * [Query](https://cloud.google.com/datastore/reference/rest/v1/projects/runQuery#query)
+ * @param array $query [optional] [Query](https://cloud.google.com/datastore/reference/rest/v1/projects/runQuery#query)
+ * @codingStandardsIgnoreEnd
*/
public function __construct(EntityMapper $entityMapper, array $query = [])
{
@@ -306,7 +306,10 @@ public function hasAncestor(Key $key)
* @see https://cloud.google.com/datastore/reference/rest/v1/projects/runQuery#Direction Allowed Directions
*
* @param string $property The property to order by.
- * @param string $direction The direction to order in.
+ * @param string $direction [optional] The direction to order in. Google
+ * Cloud PHP provides class constants which map to allowed Datastore
+ * values. Those constants are `Query::ORDER_DESCENDING` and
+ * `Query::ORDER_ASCENDING`. **Defaults to** `Query::ORDER_ACENDING`.
* @return Query
*/
public function order($property, $direction = self::ORDER_DEFAULT)
diff --git a/src/Datastore/Query/QueryInterface.php b/src/Datastore/Query/QueryInterface.php
index a620c6c7f690..aba9c66d6c73 100644
--- a/src/Datastore/Query/QueryInterface.php
+++ b/src/Datastore/Query/QueryInterface.php
@@ -20,7 +20,9 @@
use JsonSerializable;
/**
- * Represents a Datastore Query
+ * Represents a Datastore Query.
+ *
+ * @see https://cloud.google.com/datastore/docs/concepts/queries Datastore Queries
*/
interface QueryInterface extends JsonSerializable
{
diff --git a/src/Datastore/README.md b/src/Datastore/README.md
new file mode 100644
index 000000000000..9c379ef99456
--- /dev/null
+++ b/src/Datastore/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP Datastore
+
+> Idiomatic PHP client for [Cloud Datastore](https://cloud.google.com/datastore/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-datastore/latest/datastore/datastoreclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-datastore
+```
diff --git a/src/Datastore/Transaction.php b/src/Datastore/Transaction.php
index d2e24529b70b..f9cb045a7721 100644
--- a/src/Datastore/Transaction.php
+++ b/src/Datastore/Transaction.php
@@ -46,10 +46,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Datastore\DatastoreClient;
*
- * $cloud = new ServiceBuilder();
- * $datastore = $cloud->datastore();
+ * $datastore = new DatastoreClient();
*
* $transaction = $datastore->transaction();
* ```
@@ -438,7 +437,7 @@ public function lookupBatch(array $keys, array $options = [])
* Must be a subclass of {@see Google\Cloud\Datastore\Entity}.
* If not set, {@see Google\Cloud\Datastore\Entity} will be used.
* }
- * @return \Generator
+ * @return EntityIterator
*/
public function runQuery(QueryInterface $query, array $options = [])
{
@@ -458,6 +457,8 @@ public function runQuery(QueryInterface $query, array $options = [])
* $transaction->commit();
* ```
*
+ * @see https://cloud.google.com/datastore/docs/reference/rest/v1/projects/commit Commit API documentation
+ *
* @param array $options [optional] Configuration Options.
* @return array [Response Body](https://cloud.google.com/datastore/reference/rest/v1/projects/commit#response-body)
*/
diff --git a/src/Datastore/VERSION b/src/Datastore/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/Datastore/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/Datastore/composer.json b/src/Datastore/composer.json
new file mode 100644
index 000000000000..8054c0258b0d
--- /dev/null
+++ b/src/Datastore/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "google/cloud-datastore",
+ "description": "Cloud Datastore Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-datastore",
+ "target": "GoogleCloudPlatform/google-cloud-php-datastore.git",
+ "path": "src/Datastore",
+ "entry": "DatastoreClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Datastore\\": ""
+ }
+ }
+}
diff --git a/src/ErrorReporting/LICENSE b/src/ErrorReporting/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/ErrorReporting/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/ErrorReporting/README.md b/src/ErrorReporting/README.md
new file mode 100644
index 000000000000..ec6472844da0
--- /dev/null
+++ b/src/ErrorReporting/README.md
@@ -0,0 +1,14 @@
+# Stackdriver Error Reporting
+
+Stackdriver Error Reporting counts, analyzes and aggregates the crashes in your running cloud services.
+
+For more information, see [cloud.google.com](https://cloud.google.com/error-reporting/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-error-reporting/latest/errorreporting/readme)
+
+## Installation
+
+```
+$ composer require google/cloud-error-reporting
+```
diff --git a/src/ErrorReporting/V1beta1/ErrorGroupServiceClient.php b/src/ErrorReporting/V1beta1/ErrorGroupServiceClient.php
index b979d00852d4..9522bd092f6d 100644
--- a/src/ErrorReporting/V1beta1/ErrorGroupServiceClient.php
+++ b/src/ErrorReporting/V1beta1/ErrorGroupServiceClient.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
diff --git a/src/ErrorReporting/V1beta1/ErrorStatsServiceClient.php b/src/ErrorReporting/V1beta1/ErrorStatsServiceClient.php
index 76dace7aaff3..188982f4e970 100644
--- a/src/ErrorReporting/V1beta1/ErrorStatsServiceClient.php
+++ b/src/ErrorReporting/V1beta1/ErrorStatsServiceClient.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
diff --git a/src/ErrorReporting/V1beta1/README.md b/src/ErrorReporting/V1beta1/README.md
new file mode 100644
index 000000000000..6d2df572ba92
--- /dev/null
+++ b/src/ErrorReporting/V1beta1/README.md
@@ -0,0 +1,5 @@
+# Stackdriver Error Reporting
+
+Stackdriver Error Reporting counts, analyzes and aggregates the crashes in your running cloud services.
+
+For more information, see [cloud.google.com](https://cloud.google.com/error-reporting/).
diff --git a/src/ErrorReporting/V1beta1/ReportErrorsServiceClient.php b/src/ErrorReporting/V1beta1/ReportErrorsServiceClient.php
index da495d97d945..c8255296fb4d 100644
--- a/src/ErrorReporting/V1beta1/ReportErrorsServiceClient.php
+++ b/src/ErrorReporting/V1beta1/ReportErrorsServiceClient.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
diff --git a/src/ErrorReporting/V1beta1/resources/error_group_service_client_config.json b/src/ErrorReporting/V1beta1/resources/error_group_service_client_config.json
index 422c0afc4c9d..4b57386b15c9 100644
--- a/src/ErrorReporting/V1beta1/resources/error_group_service_client_config.json
+++ b/src/ErrorReporting/V1beta1/resources/error_group_service_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.devtools.clouderrorreporting.v1beta1.ErrorGroupService": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
diff --git a/src/ErrorReporting/V1beta1/resources/error_stats_service_client_config.json b/src/ErrorReporting/V1beta1/resources/error_stats_service_client_config.json
index a1449cd540de..11e42f42ca8f 100644
--- a/src/ErrorReporting/V1beta1/resources/error_stats_service_client_config.json
+++ b/src/ErrorReporting/V1beta1/resources/error_stats_service_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.devtools.clouderrorreporting.v1beta1.ErrorStatsService": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
diff --git a/src/ErrorReporting/V1beta1/resources/report_errors_service_client_config.json b/src/ErrorReporting/V1beta1/resources/report_errors_service_client_config.json
index 42b3b2a8a9dc..48b02b3d6bad 100644
--- a/src/ErrorReporting/V1beta1/resources/report_errors_service_client_config.json
+++ b/src/ErrorReporting/V1beta1/resources/report_errors_service_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.devtools.clouderrorreporting.v1beta1.ReportErrorsService": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
diff --git a/src/ErrorReporting/VERSION b/src/ErrorReporting/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/ErrorReporting/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/ErrorReporting/composer.json b/src/ErrorReporting/composer.json
new file mode 100644
index 000000000000..baa66c84d353
--- /dev/null
+++ b/src/ErrorReporting/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "google/cloud-error-reporting",
+ "description": "Stackdriver Error Reporting Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*",
+ "google/proto-client-php": "^0.9",
+ "google/gax": "^0.8"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-error-reporting",
+ "target": "GoogleCloudPlatform/google-cloud-php-errorreporting.git",
+ "path": "src/ErrorReporting",
+ "entry": null
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\ErrorReporting\\": ""
+ }
+ }
+}
diff --git a/src/Logging/Connection/Grpc.php b/src/Logging/Connection/Grpc.php
index b9fc8f6d43dd..a96d61b86874 100644
--- a/src/Logging/Connection/Grpc.php
+++ b/src/Logging/Connection/Grpc.php
@@ -18,13 +18,14 @@
namespace Google\Cloud\Logging\Connection;
use DrSlump\Protobuf\Codec\CodecInterface;
+use Google\Cloud\Core\GrpcRequestWrapper;
+use Google\Cloud\Core\GrpcTrait;
+use Google\Cloud\Core\PhpArray;
use Google\Cloud\Logging\Logger;
+use Google\Cloud\Logging\LoggingClient;
use Google\Cloud\Logging\V2\ConfigServiceV2Client;
use Google\Cloud\Logging\V2\LoggingServiceV2Client;
use Google\Cloud\Logging\V2\MetricsServiceV2Client;
-use Google\Cloud\PhpArray;
-use Google\Cloud\GrpcRequestWrapper;
-use Google\Cloud\GrpcTrait;
use google\logging\v2\LogEntry;
use google\logging\v2\LogMetric;
use google\logging\v2\LogSink;
@@ -89,19 +90,21 @@ class Grpc implements ConnectionInterface
public function __construct(array $config = [])
{
$this->codec = new PhpArray([
- 'timestamp' => function ($v) {
- return $this->formatTimestampFromApi($v);
- },
- 'severity' => function ($v) {
- return Logger::getLogLevelMap()[$v];
- },
- 'outputVersionFormat' => function ($v) {
- return self::$versionFormatMap[$v];
- }
+ 'customFilters' => [
+ 'timestamp' => function ($v) {
+ return $this->formatTimestampFromApi($v);
+ },
+ 'severity' => function ($v) {
+ return Logger::getLogLevelMap()[$v];
+ },
+ 'outputVersionFormat' => function ($v) {
+ return self::$versionFormatMap[$v];
+ }
+ ]
]);
$config['codec'] = $this->codec;
$this->setRequestWrapper(new GrpcRequestWrapper($config));
- $gaxConfig = $this->getGaxConfig();
+ $gaxConfig = $this->getGaxConfig(LoggingClient::VERSION);
$this->configClient = new ConfigServiceV2Client($gaxConfig);
$this->loggingClient = new LoggingServiceV2Client($gaxConfig);
diff --git a/src/Logging/Connection/Rest.php b/src/Logging/Connection/Rest.php
index 2529d3b9c70c..2307bbb83d6f 100644
--- a/src/Logging/Connection/Rest.php
+++ b/src/Logging/Connection/Rest.php
@@ -17,10 +17,11 @@
namespace Google\Cloud\Logging\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\RestTrait;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\UriTrait;
+use Google\Cloud\Logging\LoggingClient;
/**
* Implementation of the
@@ -39,7 +40,8 @@ class Rest implements ConnectionInterface
public function __construct(array $config = [])
{
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/logging-v2.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/logging-v2.json',
+ 'componentVersion' => LoggingClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
diff --git a/src/Logging/Entry.php b/src/Logging/Entry.php
index 0d3a9293b692..4f7bbbfc6675 100644
--- a/src/Logging/Entry.php
+++ b/src/Logging/Entry.php
@@ -24,10 +24,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Logging\LoggingClient;
*
- * $cloud = new ServiceBuilder();
- * $logging = $cloud->logging();
+ * $logging = new LoggingClient();
*
* $logger = $logging->logger('my-log');
*
@@ -58,9 +57,7 @@ public function __construct(array $info = [])
* echo $info['textPayload'];
* ```
*
- * @codingStandardsIgnoreStart
- * @see https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry LogEntry resource documentation.
- * @codingStandardsIgnoreEnd
+ * @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry LogEntry resource documentation.
*
* @return array
*/
diff --git a/src/Logging/LICENSE b/src/Logging/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/Logging/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/Logging/Logger.php b/src/Logging/Logger.php
index 2e0735c9cce0..b646632c12ae 100644
--- a/src/Logging/Logger.php
+++ b/src/Logging/Logger.php
@@ -17,19 +17,20 @@
namespace Google\Cloud\Logging;
-use Google\Cloud\ArrayTrait;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
+use Google\Cloud\Core\ValidateTrait;
use Google\Cloud\Logging\Connection\ConnectionInterface;
-use Google\Cloud\ValidateTrait;
/**
* A logger used to write entries to Google Stackdriver Logging.
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Logging\LoggingClient;
*
- * $cloud = new ServiceBuilder();
- * $logging = $cloud->logging();
+ * $logging = new LoggingClient();
*
* $logger = $logging->logger('my-log');
* ```
@@ -100,17 +101,15 @@ class Logger
private $labels;
/**
- * @codingStandardsIgnoreStart
* @param ConnectionInterface $connection Represents a connection to
* Stackdriver Logging.
* @param string $name The name of the log to write entries to.
* @param string $projectId The project's ID.
* @param array $resource [optional] The
- * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource)
+ * [monitored resource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource)
* to associate log entries with. **Defaults to** type global.
* @param array $labels [optional] A set of user-defined (key, value) data
* that provides additional information about the log entries.
- * @codingStandardsIgnoreEnd
*/
public function __construct(
ConnectionInterface $connection,
@@ -185,33 +184,37 @@ public function delete(array $options = [])
* **Defaults to** `"timestamp asc"`.
* @type int $pageSize The maximum number of results to return per
* request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function entries(array $options = [])
{
+ $resultLimit = $this->pluck('resultLimit', $options, false);
$logNameFilter = "logName = $this->formattedName";
$options += [
- 'pageToken' => null,
'resourceNames' => ["projects/$this->projectId"],
'filter' => isset($options['filter'])
? $options['filter'] .= " AND $logNameFilter"
: $logNameFilter
];
- do {
- $response = $this->connection->listEntries($options);
-
- if (!isset($response['entries'])) {
- return;
- }
-
- foreach ($response['entries'] as $entry) {
- yield new Entry($entry);
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $entry) {
+ return new Entry($entry);
+ },
+ [$this->connection, 'listEntries'],
+ $options,
+ [
+ 'itemsKey' => 'entries',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -242,8 +245,8 @@ public function entries(array $options = [])
* ]
* ]);
* ```
- * @codingStandardsIgnoreStart
- * @see https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry LogEntry resource documentation.
+ *
+ * @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry LogEntry resource documentation.
*
* @param array|string $data The data to log. When providing a string the
* data will be stored as a `textPayload` type. When providing an
@@ -252,24 +255,24 @@ public function entries(array $options = [])
* Configuration options.
*
* @type array $resource The
- * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource)
+ * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource)
* to associate this log entry with. **Defaults to** type global.
* @type array $httpRequest Information about the HTTP request
* associated with this log entry, if applicable. Please see
- * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry#httprequest)
+ * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#httprequest)
* for more information.
* @type array $labels A set of user-defined (key, value) data that
* provides additional information about the log entry.
* @type array $operation Additional information about a potentially
* long-running operation with which a log entry is associated.
- * Please see [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry#logentryoperation)
+ * Please see
+ * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#logentryoperation)
* for more information.
* @type string|int $severity The severity of the log entry. **Defaults to**
* `"DEFAULT"`.
* }
* @return Entry
* @throws \InvalidArgumentException
- * @codingStandardsIgnoreEnd
*/
public function entry($data, array $options = [])
{
diff --git a/src/Logging/LoggingClient.php b/src/Logging/LoggingClient.php
index 0eec574c0cc9..db5607db3dc5 100644
--- a/src/Logging/LoggingClient.php
+++ b/src/Logging/LoggingClient.php
@@ -17,16 +17,19 @@
namespace Google\Cloud\Logging;
-use Google\Cloud\ClientTrait;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\ClientTrait;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\Logging\Connection\ConnectionInterface;
use Google\Cloud\Logging\Connection\Grpc;
use Google\Cloud\Logging\Connection\Rest;
use Psr\Cache\CacheItemPoolInterface;
/**
- * Google Stackdriver Logging client. Allows you to store, search, analyze,
- * monitor, and alert on log data and events from Google Cloud Platform and
- * Amazon Web Services. Find more information at
+ * Google Stackdriver Logging allows you to store, search, analyze, monitor, and
+ * alert on log data and events from Google Cloud Platform and Amazon Web
+ * Services. Find more information at the
* [Google Stackdriver Logging docs](https://cloud.google.com/logging/docs/).
*
* This client supports transport over
@@ -54,14 +57,6 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- * $logging = $cloud->logging();
- * ```
- *
- * ```
- * // LoggingClient can be instantiated directly.
* use Google\Cloud\Logging\LoggingClient;
*
* $logging = new LoggingClient();
@@ -69,8 +64,11 @@
*/
class LoggingClient
{
+ use ArrayTrait;
use ClientTrait;
+ const VERSION = '0.1.0';
+
const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/logging.admin';
const READ_ONLY_SCOPE = 'https://www.googleapis.com/auth/logging.read';
const WRITE_ONLY_SCOPE = 'https://www.googleapis.com/auth/logging.write';
@@ -206,27 +204,32 @@ public function sink($name)
* @param array $options [optional] {
* Configuration options.
*
- * @type int $pageSize The maximum number of results to return per request.
+ * @type int $pageSize The maximum number of results to return per
+ * request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function sinks(array $options = [])
{
- $options['pageToken'] = null;
-
- do {
- $response = $this->connection->listSinks($options + ['parent' => $this->formattedProjectName]);
-
- if (!isset($response['sinks'])) {
- return;
- }
-
- foreach ($response['sinks'] as $sink) {
- yield new Sink($this->connection, $sink['name'], $this->projectId, $sink);
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ $resultLimit = $this->pluck('resultLimit', $options, false);
+
+ return new ItemIterator(
+ new PageIterator(
+ function (array $sink) {
+ return new Sink($this->connection, $sink['name'], $this->projectId, $sink);
+ },
+ [$this->connection, 'listSinks'],
+ $options + ['parent' => $this->formattedProjectName],
+ [
+ 'itemsKey' => 'sinks',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -302,27 +305,32 @@ public function metric($name)
* @param array $options [optional] {
* Configuration options.
*
- * @type int $pageSize The maximum number of results to return per request.
+ * @type int $pageSize The maximum number of results to return per
+ * request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function metrics(array $options = [])
{
- $options['pageToken'] = null;
-
- do {
- $response = $this->connection->listMetrics($options + ['parent' => $this->formattedProjectName]);
-
- if (!isset($response['metrics'])) {
- return;
- }
-
- foreach ($response['metrics'] as $metric) {
- yield new Metric($this->connection, $metric['name'], $this->projectId, $metric);
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ $resultLimit = $this->pluck('resultLimit', $options, false);
+
+ return new ItemIterator(
+ new PageIterator(
+ function (array $metric) {
+ return new Metric($this->connection, $metric['name'], $this->projectId, $metric);
+ },
+ [$this->connection, 'listMetrics'],
+ $options + ['parent' => $this->formattedProjectName],
+ [
+ 'itemsKey' => 'metrics',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -368,13 +376,16 @@ public function metrics(array $options = [])
* `timestamp desc`. **Defaults to** `"timestamp asc"`.
* @type int $pageSize The maximum number of results to return per
* request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function entries(array $options = [])
{
- $options['pageToken'] = null;
-
+ $resultLimit = $this->pluck('resultLimit', $options, false);
$resourceNames = ['projects/' . $this->projectId];
if (isset($options['projectIds'])) {
foreach ($options['projectIds'] as $projectId) {
@@ -388,19 +399,19 @@ public function entries(array $options = [])
$options['resourceNames'] = $resourceNames;
}
- do {
- $response = $this->connection->listEntries($options);
-
- if (!isset($response['entries'])) {
- return;
- }
-
- foreach ($response['entries'] as $entry) {
- yield new Entry($entry);
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $entry) {
+ return new Entry($entry);
+ },
+ [$this->connection, 'listEntries'],
+ $options,
+ [
+ 'itemsKey' => 'entries',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -412,7 +423,6 @@ public function entries(array $options = [])
* $psrLogger = $logging->psrLogger('my-log');
* ```
*
- * @codingStandardsIgnoreStart
* @param string $name The name of the log to write entries to.
* @param array $options [optional] {
* Configuration options.
@@ -420,13 +430,12 @@ public function entries(array $options = [])
* @type string $messageKey The key in the `jsonPayload` used to contain
* the logged message. **Defaults to** `message`.
* @type array $resource The
- * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource)
+ * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource)
* to associate log entries with. **Defaults to** type global.
* @type array $labels A set of user-defined (key, value) data that
* provides additional information about the log entry.
* }
* @return PsrLogger
- * @codingStandardsIgnoreEnd
*/
public function psrLogger($name, array $options = [])
{
@@ -450,18 +459,16 @@ public function psrLogger($name, array $options = [])
* $logger = $logging->logger('my-log');
* ```
*
- * @codingStandardsIgnoreStart
* @param string $name The name of the log to write entries to.
* @param array $options [optional] {
* Configuration options.
*
* @type array $resource The
- * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource)
+ * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource)
* to associate log entries with. **Defaults to** type global.
* @type array $labels A set of user-defined (key, value) data that
* provides additional information about the log entry.
* }
- * @codingStandardsIgnoreEnd
* @return Logger
*/
public function logger($name, array $options = [])
diff --git a/src/Logging/Metric.php b/src/Logging/Metric.php
index f3e8b7a22211..cae8460fa474 100644
--- a/src/Logging/Metric.php
+++ b/src/Logging/Metric.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Logging;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Logging\Connection\ConnectionInterface;
/**
@@ -27,10 +27,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Logging\LoggingClient;
*
- * $cloud = new ServiceBuilder();
- * $logging = $cloud->logging();
+ * $logging = new LoggingClient();
*
* $metric = $logging->metric('my-metric');
* ```
diff --git a/src/Logging/PsrLogger.php b/src/Logging/PsrLogger.php
index 19a2bfc5636d..277fd3606b0f 100644
--- a/src/Logging/PsrLogger.php
+++ b/src/Logging/PsrLogger.php
@@ -28,13 +28,14 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Logging\LoggingClient;
*
- * $cloud = new ServiceBuilder();
- * $logging = $cloud->logging();
+ * $logging = new LoggingClient();
*
* $psrLogger = $logging->psrLogger('my-log');
* ```
+ *
+ * @see http://www.php-fig.org/psr/psr-3/#psrlogloggerinterface Psr\Log\LoggerInterface
*/
class PsrLogger implements LoggerInterface
{
@@ -227,7 +228,6 @@ public function debug($message, array $context = [])
* ]);
* ```
*
- * @codingStandardsIgnoreStart
* @param string|int $level The severity of the log entry.
* @param string $message The message to log.
* @param array $context {
@@ -239,12 +239,12 @@ public function debug($message, array $context = [])
* Stackdriver specific data.
*
* @type array $stackdriverOptions['resource'] The
- * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource)
+ * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource)
* to associate this log entry with. **Defaults to** type global.
* @type array $stackdriverOptions['httpRequest'] Information about the
* HTTP request associated with this log entry, if applicable.
* Please see
- * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry#httprequest)
+ * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#httprequest)
* for more information.
* @type array $stackdriverOptions['labels'] A set of user-defined
* (key, value) data that provides additional information about
@@ -252,11 +252,10 @@ public function debug($message, array $context = [])
* @type array $stackdriverOptions['operation'] Additional information
* about a potentially long-running operation with which a log
* entry is associated. Please see
- * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry#logentryoperation)
+ * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#logentryoperation)
* for more information.
* }
* @throws InvalidArgumentException
- * @codingStandardsIgnoreEnd
*/
public function log($level, $message, array $context = [])
{
diff --git a/src/Logging/README.md b/src/Logging/README.md
new file mode 100644
index 000000000000..96c7a1a710cb
--- /dev/null
+++ b/src/Logging/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP Logging
+
+> Idiomatic PHP client for [Stackdriver Logging](https://cloud.google.com/logging/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-logging/latest/logging/loggingclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-logging
+```
diff --git a/src/Logging/Sink.php b/src/Logging/Sink.php
index 89f24c4430fb..b8107af0064b 100644
--- a/src/Logging/Sink.php
+++ b/src/Logging/Sink.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Logging;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Logging\Connection\ConnectionInterface;
/**
@@ -25,10 +25,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Logging\LoggingClient;
*
- * $cloud = new ServiceBuilder();
- * $logging = $cloud->logging();
+ * $logging = new LoggingClient();
*
* $sink = $logging->sink('my-sink');
* ```
diff --git a/src/Logging/V2/ConfigServiceV2Client.php b/src/Logging/V2/ConfigServiceV2Client.php
index 0138ef82dd34..7be1b8aab6e8 100644
--- a/src/Logging/V2/ConfigServiceV2Client.php
+++ b/src/Logging/V2/ConfigServiceV2Client.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
@@ -527,7 +534,7 @@ public function createSink($parent, $sink, $optionalArgs = [])
/**
* Updates a sink. If the named sink doesn't exist, then this method is
* identical to
- * [sinks.create](/logging/docs/api/reference/rest/v2/projects.sinks/create).
+ * [sinks.create](https://cloud.google.com/logging/docs/api/reference/rest/v2/projects.sinks/create).
* If the named sink does exist, then this method replaces the following
* fields in the existing sink with values from the new sink: `destination`,
* `filter`, `output_version_format`, `start_time`, and `end_time`.
@@ -561,7 +568,7 @@ public function createSink($parent, $sink, $optionalArgs = [])
*
* @type bool $uniqueWriterIdentity
* Optional. See
- * [sinks.create](/logging/docs/api/reference/rest/v2/projects.sinks/create)
+ * [sinks.create](https://cloud.google.com/logging/docs/api/reference/rest/v2/projects.sinks/create)
* for a description of this field. When updating a sink, the effect of this
* field on the value of `writer_identity` in the updated sink depends on both
* the old and new values of this field:
diff --git a/src/Logging/V2/LoggingServiceV2Client.php b/src/Logging/V2/LoggingServiceV2Client.php
index 87e667ec9112..4ba615cb0611 100644
--- a/src/Logging/V2/LoggingServiceV2Client.php
+++ b/src/Logging/V2/LoggingServiceV2Client.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
@@ -395,7 +402,7 @@ public function deleteLog($logName, $optionalArgs = [])
* fields.
*
* To improve throughput and to avoid exceeding the
- * [quota limit](/logging/quota-policy) for calls to `entries.write`,
+ * [quota limit](https://cloud.google.com/logging/quota-policy) for calls to `entries.write`,
* you should write multiple log entries at once rather than
* calling this method for each individual log entry.
* @param array $optionalArgs {
@@ -485,7 +492,7 @@ public function writeLogEntries($entries, $optionalArgs = [])
/**
* Lists log entries. Use this method to retrieve log entries from
* Stackdriver Logging. For ways to export log entries, see
- * [Exporting Logs](/logging/docs/export).
+ * [Exporting Logs](https://cloud.google.com/logging/docs/export).
*
* Sample code:
* ```
@@ -528,7 +535,7 @@ public function writeLogEntries($entries, $optionalArgs = [])
* `resource_names`.
* @type string $filter
* Optional. A filter that chooses which log entries to return. See [Advanced
- * Logs Filters](/logging/docs/view/advanced_filters). Only log entries that
+ * Logs Filters](https://cloud.google.com/logging/docs/view/advanced_filters). Only log entries that
* match the filter are returned. An empty filter matches all log entries in
* the resources listed in `resource_names`. Referencing a parent resource
* that is not listed in `resource_names` will cause the filter to return no
diff --git a/src/Logging/V2/MetricsServiceV2Client.php b/src/Logging/V2/MetricsServiceV2Client.php
index 546a0e971cdc..2b4746ed976b 100644
--- a/src/Logging/V2/MetricsServiceV2Client.php
+++ b/src/Logging/V2/MetricsServiceV2Client.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
diff --git a/src/Logging/V2/README.md b/src/Logging/V2/README.md
new file mode 100644
index 000000000000..b4bfdb47a63a
--- /dev/null
+++ b/src/Logging/V2/README.md
@@ -0,0 +1,5 @@
+# Stackdriver Logging
+
+Stackdriver Logging allows you to store, search, analyze, monitor, and alert on log data and events from Google Cloud Platform and Amazon Web Services (AWS).
+
+For more information, see [cloud.google.com](https://cloud.google.com/logging/).
diff --git a/src/Logging/V2/resources/config_service_v2_client_config.json b/src/Logging/V2/resources/config_service_v2_client_config.json
index bdfd9c9e1626..10e0b3d342db 100644
--- a/src/Logging/V2/resources/config_service_v2_client_config.json
+++ b/src/Logging/V2/resources/config_service_v2_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.logging.v2.ConfigServiceV2": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
diff --git a/src/Logging/V2/resources/logging_service_v2_client_config.json b/src/Logging/V2/resources/logging_service_v2_client_config.json
index b7576ee7dadd..b8a2951b3336 100644
--- a/src/Logging/V2/resources/logging_service_v2_client_config.json
+++ b/src/Logging/V2/resources/logging_service_v2_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.logging.v2.LoggingServiceV2": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
@@ -39,7 +39,12 @@
"WriteLogEntries": {
"timeout_millis": 30000,
"retry_codes_name": "non_idempotent",
- "retry_params_name": "default"
+ "retry_params_name": "default",
+ "bundling": {
+ "element_count_threshold": 100,
+ "request_byte_threshold": 1024,
+ "delay_threshold_millis": 10
+ }
},
"ListLogEntries": {
"timeout_millis": 30000,
diff --git a/src/Logging/V2/resources/metrics_service_v2_client_config.json b/src/Logging/V2/resources/metrics_service_v2_client_config.json
index 0400cb5bb638..4e6890a011d4 100644
--- a/src/Logging/V2/resources/metrics_service_v2_client_config.json
+++ b/src/Logging/V2/resources/metrics_service_v2_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.logging.v2.MetricsServiceV2": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
diff --git a/src/Logging/VERSION b/src/Logging/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/Logging/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/Logging/composer.json b/src/Logging/composer.json
new file mode 100644
index 000000000000..e5ae270c3771
--- /dev/null
+++ b/src/Logging/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "google/cloud-logging",
+ "description": "Stackdriver Logging Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "suggest": {
+ "google/gax": "Required to support gRPC",
+ "google/proto-client-php": "Required to support gRPC"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-logging",
+ "target": "GoogleCloudPlatform/google-cloud-php-logging.git",
+ "path": "src/Logging",
+ "entry": "LoggingClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Logging\\": ""
+ }
+ }
+}
diff --git a/src/Monitoring/LICENSE b/src/Monitoring/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/Monitoring/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/Monitoring/README.md b/src/Monitoring/README.md
new file mode 100644
index 000000000000..26b10e7e7abe
--- /dev/null
+++ b/src/Monitoring/README.md
@@ -0,0 +1,14 @@
+# Stackdriver Monitoring
+
+Stackdriver Monitoring provides visibility into the performance, uptime, and overall health of cloud-powered applications.
+
+For more information, see [cloud.google.com](https://cloud.google.com/monitoring/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-monitoring/latest/monitoring/readme)
+
+## Installation
+
+```
+$ composer require google/cloud-monitoring
+```
diff --git a/src/Monitoring/V3/GroupServiceClient.php b/src/Monitoring/V3/GroupServiceClient.php
index 709e537424ea..a076bdf3f8fe 100644
--- a/src/Monitoring/V3/GroupServiceClient.php
+++ b/src/Monitoring/V3/GroupServiceClient.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
@@ -695,7 +702,7 @@ public function deleteGroup($name, $optionalArgs = [])
* of values will be returned. Any page token used here must have
* been generated by a previous call to the API.
* @type string $filter
- * An optional [list filter](/monitoring/api/learn_more#filtering) describing
+ * An optional [list filter](https://cloud.google.com/monitoring/api/learn_more#filtering) describing
* the members to be returned. The filter may reference the type, labels, and
* metadata of monitored resources that comprise the group.
* For example, to return only resources representing Compute Engine VM
diff --git a/src/Monitoring/V3/MetricServiceClient.php b/src/Monitoring/V3/MetricServiceClient.php
index 8508874d5082..1b94a70d0aa5 100644
--- a/src/Monitoring/V3/MetricServiceClient.php
+++ b/src/Monitoring/V3/MetricServiceClient.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
@@ -407,7 +414,7 @@ public function __construct($options = [])
* Optional.
*
* @type string $filter
- * An optional [filter](/monitoring/api/v3/filters) describing
+ * An optional [filter](https://cloud.google.com/monitoring/api/v3/filters) describing
* the descriptors to be returned. The filter can reference
* the descriptor's type and labels. For example, the
* following filter returns only Google Compute Engine descriptors
@@ -553,10 +560,10 @@ public function getMonitoredResourceDescriptor($name, $optionalArgs = [])
* @type string $filter
* If this field is empty, all custom and
* system-defined metric descriptors are returned.
- * Otherwise, the [filter](/monitoring/api/v3/filters)
+ * Otherwise, the [filter](https://cloud.google.com/monitoring/api/v3/filters)
* specifies which metric descriptors are to be
* returned. For example, the following filter matches all
- * [custom metrics](/monitoring/custom-metrics):
+ * [custom metrics](https://cloud.google.com/monitoring/custom-metrics):
*
* metric.type = starts_with("custom.googleapis.com/")
* @type int $pageSize
@@ -667,7 +674,7 @@ public function getMetricDescriptor($name, $optionalArgs = [])
/**
* Creates a new metric descriptor.
* User-created metric descriptors define
- * [custom metrics](/monitoring/custom-metrics).
+ * [custom metrics](https://cloud.google.com/monitoring/custom-metrics).
*
* Sample code:
* ```
@@ -683,7 +690,7 @@ public function getMetricDescriptor($name, $optionalArgs = [])
*
* @param string $name The project on which to execute the request. The format is
* `"projects/{project_id_or_number}"`.
- * @param MetricDescriptor $metricDescriptor The new [custom metric](/monitoring/custom-metrics)
+ * @param MetricDescriptor $metricDescriptor The new [custom metric](https://cloud.google.com/monitoring/custom-metrics)
* descriptor.
* @param array $optionalArgs {
* Optional.
@@ -724,7 +731,7 @@ public function createMetricDescriptor($name, $metricDescriptor, $optionalArgs =
/**
* Deletes a metric descriptor. Only user-created
- * [custom metrics](/monitoring/custom-metrics) can be deleted.
+ * [custom metrics](https://cloud.google.com/monitoring/custom-metrics) can be deleted.
*
* Sample code:
* ```
@@ -806,7 +813,7 @@ public function deleteMetricDescriptor($name, $optionalArgs = [])
*
* @param string $name The project on which to execute the request. The format is
* "projects/{project_id_or_number}".
- * @param string $filter A [monitoring filter](/monitoring/api/v3/filters) that specifies which time
+ * @param string $filter A [monitoring filter](https://cloud.google.com/monitoring/api/v3/filters) that specifies which time
* series should be returned. The filter must specify a single metric type,
* and can additionally specify metric labels and other information. For
* example:
diff --git a/src/Monitoring/V3/README.md b/src/Monitoring/V3/README.md
new file mode 100644
index 000000000000..b6936242f5b0
--- /dev/null
+++ b/src/Monitoring/V3/README.md
@@ -0,0 +1,5 @@
+# Stackdriver Monitoring
+
+Stackdriver Monitoring provides visibility into the performance, uptime, and overall health of cloud-powered applications.
+
+For more information, see [cloud.google.com](https://cloud.google.com/monitoring/).
diff --git a/src/Monitoring/V3/resources/group_service_client_config.json b/src/Monitoring/V3/resources/group_service_client_config.json
index c1d5d808d100..825193b3a411 100644
--- a/src/Monitoring/V3/resources/group_service_client_config.json
+++ b/src/Monitoring/V3/resources/group_service_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.monitoring.v3.GroupService": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
diff --git a/src/Monitoring/V3/resources/metric_service_client_config.json b/src/Monitoring/V3/resources/metric_service_client_config.json
index 4ec14c73bde7..b04f72277007 100644
--- a/src/Monitoring/V3/resources/metric_service_client_config.json
+++ b/src/Monitoring/V3/resources/metric_service_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.monitoring.v3.MetricService": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
diff --git a/src/Monitoring/VERSION b/src/Monitoring/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/Monitoring/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/Monitoring/composer.json b/src/Monitoring/composer.json
new file mode 100644
index 000000000000..9ed5a34bcfb0
--- /dev/null
+++ b/src/Monitoring/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "google/cloud-monitoring",
+ "description": "Stackdriver Monitoring Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*",
+ "google/proto-client-php": "^0.9",
+ "google/gax": "^0.8"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-monitoring",
+ "target": "GoogleCloudPlatform/google-cloud-php-monitoring.git",
+ "path": "src/Monitoring",
+ "entry": null
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Monitoring\\": ""
+ }
+ }
+}
diff --git a/src/NaturalLanguage/Annotation.php b/src/NaturalLanguage/Annotation.php
index b14b10fae519..de2739e83ee4 100644
--- a/src/NaturalLanguage/Annotation.php
+++ b/src/NaturalLanguage/Annotation.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\NaturalLanguage;
-use Google\Cloud\CallTrait;
+use Google\Cloud\Core\CallTrait;
/**
* Annotations represent the result of a request against the
@@ -34,6 +34,15 @@
* {@see Google\Cloud\NaturalLanguage\NaturalLanguageClient::analyzeSyntax()} and
* {@see Google\Cloud\NaturalLanguage\NaturalLanguageClient::annotateText()}.
*
+ * Example:
+ * ```
+ * use Google\Cloud\NaturalLanguage\NaturalLanguageClient;
+ *
+ * $language = new NaturalLanguageClient();
+ *
+ * $annotation = $language->annotateText('Google Cloud Platform is a powerful tool.');
+ * ```
+ *
* @method sentences() {
* Returns an array of sentences found in the document.
*
@@ -44,9 +53,7 @@
* }
* ```
*
- * @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#Sentence Sentence type documentation
- * @codingStandardsIgnoreEnd
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Sentence Sentence type documentation
*
* @return array|null
* }
@@ -60,9 +67,7 @@
* }
* ```
*
- * @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#Token Token type documentation
- * @codingStandardsIgnoreEnd
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Token Token type documentation
*
* @return array|null
* }
@@ -76,7 +81,7 @@
* }
* ```
*
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/Entity Entity type documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Entity Entity type documentation
*
* @return array|null
* }
@@ -119,7 +124,7 @@ public function __construct(array $info = [])
* ```
*
* @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#response-body Annotate Text documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/annotateText#response-body Annotate Text documentation
* @codingStandardsIgnoreEnd
*
* @return array
@@ -141,7 +146,7 @@ public function info()
* }
* ```
*
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/Sentiment Sentiment type documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Sentiment Sentiment type documentation
*
* @return array|null
*/
@@ -163,7 +168,7 @@ public function sentiment()
* ```
*
* @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#tag Token tags documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Token#Tag Token tags documentation
* @codingStandardsIgnoreEnd
*
* @return array|null
@@ -190,7 +195,7 @@ public function tokensByTag($tag)
* ```
*
* @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#label Token labels documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Token#Label Token labels documentation
* @codingStandardsIgnoreEnd
*
* @return array|null
@@ -217,7 +222,7 @@ public function tokensByLabel($label)
* ```
*
* @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/Entity#type Entity types documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Entity#Type Entity types documentation
* @codingStandardsIgnoreEnd
*
* @return array|null
diff --git a/src/NaturalLanguage/Connection/Rest.php b/src/NaturalLanguage/Connection/Rest.php
index 01d2622c62e1..7b34d7656546 100644
--- a/src/NaturalLanguage/Connection/Rest.php
+++ b/src/NaturalLanguage/Connection/Rest.php
@@ -17,10 +17,11 @@
namespace Google\Cloud\NaturalLanguage\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\RestTrait;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\UriTrait;
+use Google\Cloud\NaturalLanguage\NaturalLanguageClient;
/**
* Implementation of the
@@ -39,7 +40,8 @@ class Rest implements ConnectionInterface
public function __construct(array $config = [])
{
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/language-v1.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/language-v1.json',
+ 'componentVersion' => NaturalLanguageClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
diff --git a/src/NaturalLanguage/LICENSE b/src/NaturalLanguage/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/NaturalLanguage/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/NaturalLanguage/NaturalLanguageClient.php b/src/NaturalLanguage/NaturalLanguageClient.php
index 7d096e607d57..9a16a98005e4 100644
--- a/src/NaturalLanguage/NaturalLanguageClient.php
+++ b/src/NaturalLanguage/NaturalLanguageClient.php
@@ -17,30 +17,21 @@
namespace Google\Cloud\NaturalLanguage;
-use Google\Cloud\ClientTrait;
+use Google\Cloud\Core\ClientTrait;
use Google\Cloud\NaturalLanguage\Connection\ConnectionInterface;
use Google\Cloud\NaturalLanguage\Connection\Rest;
use Google\Cloud\Storage\StorageObject;
use Psr\Cache\CacheItemPoolInterface;
/**
- * Google Cloud Natural Language client. Provides natural language understanding
+ * Google Cloud Natural Language provides natural language understanding
* technologies to developers, including sentiment analysis, entity recognition,
* and syntax analysis. Currently only English, Spanish, and Japanese textual
- * context are supported. Find more information at
+ * context are supported. Find more information at the
* [Google Cloud Natural Language docs](https://cloud.google.com/natural-language/docs/).
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- *
- * $language = $cloud->naturalLanguage();
- * ```
- *
- * ```
- * // NaturalLanguage can be instantiated directly.
* use Google\Cloud\NaturalLanguage\NaturalLanguageClient;
*
* $language = new NaturalLanguageClient();
@@ -50,6 +41,8 @@ class NaturalLanguageClient
{
use ClientTrait;
+ const VERSION = '0.1.0';
+
const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
/**
@@ -117,21 +110,37 @@ public function __construct(array $config = [])
* ```
*
* @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/analyzeEntities Analyze Entities API documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeEntities Analyze Entities API documentation
* @codingStandardsIgnoreEnd
*
- * @param string|StorageObject $content The content to analyze.
+ * @param string|StorageObject $content The content to analyze. May be
+ * either a string of UTF-8 encoded content, a URI pointing to a
+ * Google Cloud Storage object in the format of
+ * `gs://{bucket-name}/{object-name}` or a
+ * {@see Google\Cloud\Storage\StorageObject}.
* @param array $options [optional] {
* Configuration options.
*
+ * @type bool $detectGcsUri When providing $content as a string, this
+ * flag determines whether or not to attempt to detect if the
+ * string represents a Google Cloud Storage URI in the format of
+ * `gs://{bucket-name}/{object-name}`. **Defaults to** `true`.
* @type string $type The document type. Acceptable values are
* `PLAIN_TEXT` or `HTML`. **Defaults to** `"PLAIN_TEXT"`.
* @type string $language The language of the document. Both ISO
* (e.g., en, es) and BCP-47 (e.g., en-US, es-ES) language codes
- * are accepted. Defaults to `"en"` (English).
+ * are accepted. If no value is provided, the language will be
+ * detected by the service.
* @type string $encodingType The text encoding type used by the API to
* calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`,
- * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`.
+ * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. Please note
+ * the following behaviors for the encoding type setting: `"NONE"`
+ * will return a value of "-1" for offsets. `"UTF8"` will
+ * return byte offsets. `"UTF16"` will return
+ * [code unit](http://unicode.org/glossary/#code_unit) offsets.
+ * `"UTF32"` will return
+ * [unicode character](http://unicode.org/glossary/#character)
+ * offsets.
* }
* @return Annotation
*/
@@ -158,13 +167,21 @@ public function analyzeEntities($content, array $options = [])
* ```
*
* @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/analyzeSentiment Analyze Sentiment API documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeSentiment Analyze Sentiment API documentation
* @codingStandardsIgnoreEnd
*
- * @param string|StorageObject $content The content to analyze.
+ * @param string|StorageObject $content The content to analyze. May be
+ * either a string of UTF-8 encoded content, a URI pointing to a
+ * Google Cloud Storage object in the format of
+ * `gs://{bucket-name}/{object-name}` or a
+ * {@see Google\Cloud\Storage\StorageObject}.
* @param array $options [optional] {
* Configuration options.
*
+ * @type bool $detectGcsUri When providing $content as a string, this
+ * flag determines whether or not to attempt to detect if the
+ * string represents a Google Cloud Storage URI in the format of
+ * `gs://{bucket-name}/{object-name}`. **Defaults to** `true`.
* @type string $type The document type. Acceptable values are
* `PLAIN_TEXT` or `HTML`. **Defaults to** `"PLAIN_TEXT"`.
* @type string $language The language of the document. Both ISO
@@ -173,7 +190,14 @@ public function analyzeEntities($content, array $options = [])
* detected by the service.
* @type string $encodingType The text encoding type used by the API to
* calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`,
- * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`.
+ * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. Please note
+ * the following behaviors for the encoding type setting: `"NONE"`
+ * will return a value of "-1" for offsets. `"UTF8"` will
+ * return byte offsets. `"UTF16"` will return
+ * [code unit](http://unicode.org/glossary/#code_unit) offsets.
+ * `"UTF32"` will return
+ * [unicode character](http://unicode.org/glossary/#character)
+ * offsets.
* }
* @return Annotation
*/
@@ -199,13 +223,21 @@ public function analyzeSentiment($content, array $options = [])
* ```
*
* @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/analyzeSyntax Analyze Syntax API documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeSyntax Analyze Syntax API documentation
* @codingStandardsIgnoreEnd
*
- * @param string|StorageObject $content The content to analyze.
+ * @param string|StorageObject $content The content to analyze. May be
+ * either a string of UTF-8 encoded content, a URI pointing to a
+ * Google Cloud Storage object in the format of
+ * `gs://{bucket-name}/{object-name}` or a
+ * {@see Google\Cloud\Storage\StorageObject}.
* @param array $options [optional] {
* Configuration options.
*
+ * @type bool $detectGcsUri When providing $content as a string, this
+ * flag determines whether or not to attempt to detect if the
+ * string represents a Google Cloud Storage URI in the format of
+ * `gs://{bucket-name}/{object-name}`. **Defaults to** `true`.
* @type string $type The document type. Acceptable values are
* `PLAIN_TEXT` or `HTML`. **Defaults to** `"PLAIN_TEXT"`.
* @type string $language The language of the document. Both ISO
@@ -214,7 +246,14 @@ public function analyzeSentiment($content, array $options = [])
* detected by the service.
* @type string $encodingType The text encoding type used by the API to
* calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`,
- * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`.
+ * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. Please note
+ * the following behaviors for the encoding type setting: `"NONE"`
+ * will return a value of "-1" for offsets. `"UTF8"` will
+ * return byte offsets. `"UTF16"` will return
+ * [code unit](http://unicode.org/glossary/#code_unit) offsets.
+ * `"UTF32"` will return
+ * [unicode character](http://unicode.org/glossary/#character)
+ * offsets.
* }
* @return Annotation
*/
@@ -252,13 +291,21 @@ public function analyzeSyntax($content, array $options = [])
* ```
*
* @codingStandardsIgnoreStart
- * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText Annotate Text API documentation
+ * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/annotateText Annotate Text API documentation
* @codingStandardsIgnoreEnd
*
- * @param string|StorageObject $content The content to annotate.
+ * @param string|StorageObject $content The content to analyze. May be
+ * either a string of UTF-8 encoded content, a URI pointing to a
+ * Google Cloud Storage object in the format of
+ * `gs://{bucket-name}/{object-name}` or a
+ * {@see Google\Cloud\Storage\StorageObject}.
* @param array $options [optional] {
* Configuration options.
*
+ * @type bool $detectGcsUri When providing $content as a string, this
+ * flag determines whether or not to attempt to detect if the
+ * string represents a Google Cloud Storage URI in the format of
+ * `gs://{bucket-name}/{object-name}`. **Defaults to** `true`.
* @type array $features Features to apply to the request. Valid values
* are `syntax`, `sentiment`, and `entities`. If no features are
* provided the request will run with all three enabled.
@@ -270,7 +317,14 @@ public function analyzeSyntax($content, array $options = [])
* detected by the service.
* @type string $encodingType The text encoding type used by the API to
* calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`,
- * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`.
+ * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. Please note
+ * the following behaviors for the encoding type setting: `"NONE"`
+ * will return a value of "-1" for offsets. `"UTF8"` will
+ * return byte offsets. `"UTF16"` will return
+ * [code unit](http://unicode.org/glossary/#code_unit) offsets.
+ * `"UTF32"` will return
+ * [unicode character](http://unicode.org/glossary/#character)
+ * offsets.
* }
* @return Annotation
*/
@@ -321,16 +375,20 @@ private function formatRequest($content, array $options)
$docOptions = ['type', 'language', 'content', 'gcsContentUri'];
$options += [
'encodingType' => 'UTF8',
- 'type' => 'PLAIN_TEXT'
+ 'type' => 'PLAIN_TEXT',
+ 'detectGcsUri' => true
];
if ($content instanceof StorageObject) {
- $objIdentity = $content->identity();
- $options['gcsContentUri'] = 'gs://' . $objIdentity['bucket'] . '/' . $objIdentity['object'];
+ $options['gcsContentUri'] = $content->gcsUri();
+ } elseif ($options['detectGcsUri'] && substr($content, 0, 5) === 'gs://') {
+ $options['gcsContentUri'] = $content;
} else {
$options['content'] = $content;
}
+ unset($options['detectGcsUri']);
+
foreach ($options as $option => $value) {
if (in_array($option, $docOptions)) {
$options['document'][$option] = $value;
diff --git a/src/NaturalLanguage/README.md b/src/NaturalLanguage/README.md
new file mode 100644
index 000000000000..3dbfe926eb70
--- /dev/null
+++ b/src/NaturalLanguage/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP Natural Language
+
+> Idiomatic PHP client for [Cloud Natural Language](https://cloud.google.com/natural-language/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-natural-language/latest/naturallanguage/naturallanguageclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-natural-language
+```
diff --git a/src/NaturalLanguage/VERSION b/src/NaturalLanguage/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/NaturalLanguage/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/NaturalLanguage/composer.json b/src/NaturalLanguage/composer.json
new file mode 100644
index 000000000000..a56720b04807
--- /dev/null
+++ b/src/NaturalLanguage/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "google/cloud-natural-language",
+ "description": "Cloud Natural Language Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "suggest": {
+ "google/cloud-storage": "Analyze documents stored in Google Cloud Storage"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-natural-language",
+ "target": "GoogleCloudPlatform/google-cloud-php-naturallanguage.git",
+ "path": "src/NaturalLanguage",
+ "entry": "NaturalLanguageClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\NaturalLanguage\\": ""
+ }
+ }
+}
diff --git a/src/PubSub/Connection/Grpc.php b/src/PubSub/Connection/Grpc.php
index 3731bfb88812..c79685e24493 100644
--- a/src/PubSub/Connection/Grpc.php
+++ b/src/PubSub/Connection/Grpc.php
@@ -18,12 +18,13 @@
namespace Google\Cloud\PubSub\Connection;
use DrSlump\Protobuf\Codec\CodecInterface;
-use Google\Cloud\EmulatorTrait;
-use Google\Cloud\PhpArray;
+use Google\Cloud\Core\EmulatorTrait;
+use Google\Cloud\Core\GrpcRequestWrapper;
+use Google\Cloud\Core\GrpcTrait;
+use Google\Cloud\Core\PhpArray;
+use Google\Cloud\PubSub\PubSubClient;
use Google\Cloud\PubSub\V1\PublisherClient;
use Google\Cloud\PubSub\V1\SubscriberClient;
-use Google\Cloud\GrpcRequestWrapper;
-use Google\Cloud\GrpcTrait;
use Grpc\ChannelCredentials;
use google\iam\v1\Policy;
use google\pubsub\v1\PubsubMessage;
@@ -60,12 +61,16 @@ class Grpc implements ConnectionInterface
*/
public function __construct(array $config = [])
{
- $this->codec = new PhpArray(['publishTime' => function ($v) {
- return $this->formatTimestampFromApi($v);
- }]);
+ $this->codec = new PhpArray([
+ 'customFilters' => [
+ 'publishTime' => function ($v) {
+ return $this->formatTimestampFromApi($v);
+ }
+ ]
+ ]);
$config['codec'] = $this->codec;
$this->setRequestWrapper(new GrpcRequestWrapper($config));
- $grpcConfig = $this->getGaxConfig();
+ $grpcConfig = $this->getGaxConfig(PubSubClient::VERSION);
$emulatorHost = getenv('PUBSUB_EMULATOR_HOST');
$baseUri = $this->getEmulatorBaseUri(self::BASE_URI, $emulatorHost);
diff --git a/src/PubSub/Connection/IamSubscription.php b/src/PubSub/Connection/IamSubscription.php
index 261a8d948a19..e4693f23dcd8 100644
--- a/src/PubSub/Connection/IamSubscription.php
+++ b/src/PubSub/Connection/IamSubscription.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\PubSub\Connection;
-use Google\Cloud\Iam\IamConnectionInterface;
+use Google\Cloud\Core\Iam\IamConnectionInterface;
/**
* Proxy IAM service calls to the Pub/Sub Subscription IAM resources
diff --git a/src/PubSub/Connection/IamTopic.php b/src/PubSub/Connection/IamTopic.php
index 151ad3b67cc4..5efe89c760d1 100644
--- a/src/PubSub/Connection/IamTopic.php
+++ b/src/PubSub/Connection/IamTopic.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\PubSub\Connection;
-use Google\Cloud\Iam\IamConnectionInterface;
+use Google\Cloud\Core\Iam\IamConnectionInterface;
/**
* Proxy IAM service calls to the Pub/Sub Topic IAM resources
diff --git a/src/PubSub/Connection/Rest.php b/src/PubSub/Connection/Rest.php
index 681d1b78572d..76bfb3f48090 100644
--- a/src/PubSub/Connection/Rest.php
+++ b/src/PubSub/Connection/Rest.php
@@ -17,11 +17,12 @@
namespace Google\Cloud\PubSub\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\EmulatorTrait;
-use Google\Cloud\RestTrait;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\EmulatorTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\UriTrait;
+use Google\Cloud\PubSub\PubSubClient;
/**
* Implementation of the
@@ -52,7 +53,8 @@ public function __construct(array $config = [])
}
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/pubsub-v1.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/pubsub-v1.json',
+ 'componentVersion' => PubSubClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
diff --git a/src/PubSub/IncomingMessageTrait.php b/src/PubSub/IncomingMessageTrait.php
index b1b80b884f6c..025e3eff6414 100644
--- a/src/PubSub/IncomingMessageTrait.php
+++ b/src/PubSub/IncomingMessageTrait.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\PubSub;
-use Google\Cloud\Exception\GoogleException;
+use Google\Cloud\Core\Exception\GoogleException;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
/**
diff --git a/src/PubSub/LICENSE b/src/PubSub/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/PubSub/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/PubSub/Message.php b/src/PubSub/Message.php
index 973218018daf..f3ca7330ab03 100644
--- a/src/PubSub/Message.php
+++ b/src/PubSub/Message.php
@@ -22,10 +22,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\PubSub\PubSubClient;
*
- * $cloud = new ServiceBuilder();
- * $pubsub = $cloud->pubsub();
+ * $pubsub = new PubSubClient();
* $subscription = $pubsub->subscription('my-new-subscription');
*
* $messages = $subscription->pull();
diff --git a/src/PubSub/PubSubClient.php b/src/PubSub/PubSubClient.php
index 9e539b09f508..c365246028c1 100644
--- a/src/PubSub/PubSubClient.php
+++ b/src/PubSub/PubSubClient.php
@@ -17,15 +17,18 @@
namespace Google\Cloud\PubSub;
-use Google\Cloud\ClientTrait;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\ClientTrait;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\PubSub\Connection\Grpc;
use Google\Cloud\PubSub\Connection\Rest;
use InvalidArgumentException;
use Psr\Cache\CacheItemPoolInterface;
/**
- * Google Cloud Pub/Sub client. Allows you to send and receive
- * messages between independent applications. Find more information at
+ * Google Cloud Pub/Sub allows you to send and receive
+ * messages between independent applications. Find more information at the
* [Google Cloud Pub/Sub docs](https://cloud.google.com/pubsub/docs/).
*
* To enable the [Google Cloud Pub/Sub Emulator](https://cloud.google.com/pubsub/emulator),
@@ -57,15 +60,6 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- *
- * $pubsub = $cloud->pubsub();
- * ```
- *
- * ```
- * // PubSubClient can be instantiated directly.
* use Google\Cloud\PubSub\PubSubClient;
*
* $pubsub = new PubSubClient();
@@ -73,22 +67,24 @@
*
* ```
* // Using the Pub/Sub Emulator
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\PubSub\PubSubClient;
*
* // Be sure to use the port specified when starting the emulator.
* // `8900` is used as an example only.
* putenv('PUBSUB_EMULATOR_HOST=http://localhost:8900');
*
- * $cloud = new ServiceBuilder();
- * $pubsub = $cloud->pubsub();
+ * $pubsub = new PubSubClient();
* ```
*/
class PubSubClient
{
+ use ArrayTrait;
use ClientTrait;
use IncomingMessageTrait;
use ResourceNameTrait;
+ const VERSION = '0.1.0';
+
const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/pubsub';
/**
@@ -216,27 +212,30 @@ public function topic($name)
*
* @type int $pageSize Maximum number of results to return per
* request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return Generator
+ * @return ItemIterator
*/
public function topics(array $options = [])
{
- $options['pageToken'] = null;
-
- do {
- $response = $this->connection->listTopics($options + [
- 'project' => $this->formatName('project', $this->projectId)
- ]);
+ $resultLimit = $this->pluck('resultLimit', $options, false);
- foreach ($response['topics'] as $topic) {
- yield $this->topicFactory($topic['name'], $topic);
- }
-
- // If there's a page token, we'll request the next page.
- $options['pageToken'] = isset($response['nextPageToken'])
- ? $response['nextPageToken']
- : null;
- } while ($options['pageToken']);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $topic) {
+ return $this->topicFactory($topic['name'], $topic);
+ },
+ [$this->connection, 'listTopics'],
+ $options + ['project' => $this->formatName('project', $this->projectId)],
+ [
+ 'itemsKey' => 'topics',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
@@ -312,31 +311,34 @@ public function subscription($name, $topicName = null)
*
* @type int $pageSize Maximum number of results to return per
* request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function subscriptions(array $options = [])
{
- $options['pageToken'] = null;
+ $resultLimit = $this->pluck('resultLimit', $options, false);
- do {
- $response = $this->connection->listSubscriptions($options + [
- 'project' => $this->formatName('project', $this->projectId)
- ]);
-
- foreach ($response['subscriptions'] as $subscription) {
- yield $this->subscriptionFactory(
- $subscription['name'],
- $subscription['topic'],
- $subscription
- );
- }
-
- // If there's a page token, we'll request the next page.
- $options['pageToken'] = isset($response['nextPageToken'])
- ? $response['nextPageToken']
- : null;
- } while ($options['pageToken']);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $subscription) {
+ return $this->subscriptionFactory(
+ $subscription['name'],
+ $subscription['topic'],
+ $subscription
+ );
+ },
+ [$this->connection, 'listSubscriptions'],
+ $options + ['project' => $this->formatName('project', $this->projectId)],
+ [
+ 'itemsKey' => 'subscriptions',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
diff --git a/src/PubSub/README.md b/src/PubSub/README.md
index 6d15710106df..276399880233 100644
--- a/src/PubSub/README.md
+++ b/src/PubSub/README.md
@@ -1,135 +1,16 @@
-# Google Cloud Pub/Sub
+# Google Cloud PHP PubSub
-Google Cloud Pub/Sub allows you to send and receive messages between independent
-applications.
+> Idiomatic PHP client for [Cloud Pub/Sub](https://cloud.google.com/pubsub/).
-## More Information
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-pubsub/latest/pubsub/pubsubclient)
-* [Google Cloud Pub/Sub docs](https://cloud.google.com/pubsub/docs/).
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
-## Client Libraries
+## Installation
-* [**PubSubClient**](https://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/pubsub/pubsubclient)
-* [Subscription](https://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/pubsub/subscription)
-* [Topic](https://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/pubsub/topic)
-
-## Generated Client
-
-[What are Generated Clients?](https://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/guides/generated-client)
-
-### V1
-
-* [PublisherApi](https://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/pubsub/v1/publisherapi)
-* [SubscriberApi](https://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/pubsub/v1/subscriberapi)
-
-## Using Pub/Sub
-
-### Concepts
-
-Pub/Sub is a way of sharing messages between independent applications. A
-"Publisher" will push messages into a topic, while a "Subscriber" will retrieve
-messages from a subscription.
-
-Subscriptions are a named resource representing the stream of messages from a
-single topic. Generally, each subscribing application would create a single
-subscription to each topic which is applicable to that application.
-
-Messages are a combination of data and optional attributes which a publisher
-sends to a topic to be delivered to each subscriber.
-
-Subscribers may elect to receive messages by pulling (i.e. sending a request,
-to which the response contains all new messages), or by push (by supplying an
-application endpoint which can consume incoming messages on demand).
-
-### Examples
-
-#### Obtaining an instance of `PubSubClient`
-
-```php
-pubsub();
-```
-
-#### Creating a new Topic
-
-```php
-$pubsub->createTopic('my-new-topic');
-```
-
-#### Publish a message
-
-`PubSubClient::createTopic()` should be called *once* to create the named topic
-in the service. To obtain a reference to an *existing* topic, use
-`PubSubClient::topic()`.
-
-```php
-$topic = $pubsub->topic('my-new-topic');
-$topic->publish([
- 'data' => 'Hello world!',
- 'attributes' => [
- 'key' => 'val'
- ]
-);
-```
-
-#### Subscribe to a topic
-
-```php
-$subscription = $pubsub->subscribe('my-new-subscription');
```
-
-#### Retrieve messages
-
-`PubSubClient::subscribe()` should be called *once* to create the named
-subscription in the service. To obtain a reference to an *existing*
-subscription, use `PubSubClient::subscription()`.
-
-```php
-$subscription = $pubsub->subscription('my-new-subscription');
-$messages = $subscription->pull();
-
-foreach ($messages as $message) {
- echo $message['message']['data'];
-}
-```
-
-#### Acknowledge received message
-
-Received messages should be *acknowledged* upon receipt, to prevent them from
-being delivered multiple times.
-
-```php
-$subscription = $pubsub->subscription('my-new-subscription');
-$messages = $subscription->pull();
-
-$ackIds = [];
-foreach ($messages as $message) {
- $ackIds[] = $message['ackId'];
-}
-
-$subscription->acknowledgeBatch($ackIds);
-```
-
-#### Configure Push Delivery
-
-When Push Delivery is enabled, new messages will be delivered via HTTP to the
-given endpoint.
-
-For more information, refer to the [receive push](https://cloud.google.com/pubsub/docs/subscriber#receive_push)
-guide.
-
-To pause push delivery for a subscription, call
-`Subscription::modifyPushConfig()`, supplying an empty string (`''`) as the
-`pushEndpoint`.
-
-```php
-$subscription = $pubsub->subscription('my-new-subscription');
-$subscription->modifyPushConfig([
- 'pushEndpoint' => 'https://mysite.com/application/pubsub/push'
-]);
+$ composer require google/cloud-pubsub
```
diff --git a/src/PubSub/ResourceNameTrait.php b/src/PubSub/ResourceNameTrait.php
index 7ad697f69a5b..9a2496d4852d 100644
--- a/src/PubSub/ResourceNameTrait.php
+++ b/src/PubSub/ResourceNameTrait.php
@@ -55,7 +55,7 @@ trait ResourceNameTrait
* @return string
* @throws \InvalidArgumentException
*/
- public function pluckName($type, $name)
+ private function pluckName($type, $name)
{
if (!isset($this->regexes[$type])) {
throw new InvalidArgumentException(sprintf(
@@ -85,7 +85,7 @@ public function pluckName($type, $name)
* @return string
* @throws \InvalidArgumentException
*/
- public function formatName($type, $name, $projectId = null)
+ private function formatName($type, $name, $projectId = null)
{
if (!isset($this->templates[$type])) {
throw new InvalidArgumentException(sprintf(
@@ -113,7 +113,7 @@ public function formatName($type, $name, $projectId = null)
* @return bool
* @throws \InvalidArgumentException
*/
- public function isFullyQualifiedName($type, $name)
+ private function isFullyQualifiedName($type, $name)
{
if (!isset($this->regexes[$type])) {
throw new InvalidArgumentException(sprintf(
diff --git a/src/PubSub/Subscription.php b/src/PubSub/Subscription.php
index 4dcf3910ceab..5c6ee7128b10 100644
--- a/src/PubSub/Subscription.php
+++ b/src/PubSub/Subscription.php
@@ -17,12 +17,12 @@
namespace Google\Cloud\PubSub;
-use Google\Cloud\Exception\NotFoundException;
-use Google\Cloud\Iam\Iam;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Core\Iam\Iam;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
use Google\Cloud\PubSub\Connection\IamSubscription;
use Google\Cloud\PubSub\IncomingMessageTrait;
-use Google\Cloud\ValidateTrait;
+use Google\Cloud\Core\ValidateTrait;
use InvalidArgumentException;
/**
@@ -32,11 +32,9 @@
* Example:
* ```
* // Create subscription through a topic
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
+ * use Google\Cloud\PubSub\PubSubClient;
*
- * $pubsub = $cloud->pubsub();
+ * $pubsub = new PubSubClient();
*
* $topic = $pubsub->topic('my-new-topic');
*
@@ -45,7 +43,6 @@
*
* ```
* // Create subscription through PubSubClient
- *
* use Google\Cloud\PubSub\PubSubClient;
*
* $pubsub = new PubSubClient();
@@ -331,42 +328,36 @@ public function reload(array $options = [])
* @param array $options [optional] {
* Configuration Options
*
- * @type bool $returnImmediately If set, the system will respond
+ * @type bool $returnImmediately If true, the system will respond
* immediately, even if no messages are available. Otherwise,
- * wait until new messages are available.
- * @type int $maxMessages Limit the amount of messages pulled.
+ * wait until new messages are available. **Defaults to**
+ * `false`.
+ * @type int $maxMessages Limit the amount of messages pulled.
+ * **Defaults to** `1000`.
* }
- * @codingStandardsIgnoreStart
- * @return \Generator
- * @codingStandardsIgnoreEnd
+ * @return Message[]
*/
public function pull(array $options = [])
{
- $options['pageToken'] = null;
+ $messages = [];
$options['returnImmediately'] = isset($options['returnImmediately'])
? $options['returnImmediately']
: false;
-
$options['maxMessages'] = isset($options['maxMessages'])
? $options['maxMessages']
: self::MAX_MESSAGES;
- do {
- $response = $this->connection->pull($options + [
- 'subscription' => $this->name
- ]);
+ $response = $this->connection->pull($options + [
+ 'subscription' => $this->name
+ ]);
- if (isset($response['receivedMessages'])) {
- foreach ($response['receivedMessages'] as $message) {
- yield $this->messageFactory($message, $this->connection, $this->projectId, $this->encode);
- }
+ if (isset($response['receivedMessages'])) {
+ foreach ($response['receivedMessages'] as $message) {
+ $messages[] = $this->messageFactory($message, $this->connection, $this->projectId, $this->encode);
}
+ }
- // If there's a page token, we'll request the next page.
- $options['pageToken'] = isset($response['nextPageToken'])
- ? $response['nextPageToken']
- : null;
- } while ($options['pageToken']);
+ return $messages;
}
/**
@@ -378,9 +369,8 @@ public function pull(array $options = [])
* Example:
* ```
* $messages = $subscription->pull();
- * $messagesArray = iterator_to_array($messages);
*
- * $subscription->acknowledge($messagesArray[0]);
+ * $subscription->acknowledge($messages[0]);
* ```
*
* @codingStandardsIgnoreStart
@@ -405,9 +395,8 @@ public function acknowledge(Message $message, array $options = [])
* Example:
* ```
* $messages = $subscription->pull();
- * $messagesArray = iterator_to_array($messages);
*
- * $subscription->acknowledgeBatch($messagesArray);
+ * $subscription->acknowledgeBatch($messages);
* ```
*
* @codingStandardsIgnoreStart
@@ -477,16 +466,15 @@ public function modifyAckDeadline(Message $message, $seconds, array $options = [
* Example:
* ```
* $messages = $subscription->pull();
- * $messagesArray = iterator_to_array($messages);
*
- * // Set the ack deadline to a minute and a half from now for every message
- * $subscription->modifyAckDeadlineBatch($messagesArray, 3);
+ * // Set the ack deadline to three seconds from now for every message
+ * $subscription->modifyAckDeadlineBatch($messages, 3);
*
* // Delay execution, or make a sandwich or something.
* sleep(2);
*
* // Now we'll acknowledge
- * $subscription->acknowledge($messagesArray);
+ * $subscription->acknowledgeBatch($messages);
* ```
*
* @codingStandardsIgnoreStart
diff --git a/src/PubSub/Topic.php b/src/PubSub/Topic.php
index 2afaf8c16487..5a689b03819f 100644
--- a/src/PubSub/Topic.php
+++ b/src/PubSub/Topic.php
@@ -17,8 +17,11 @@
namespace Google\Cloud\PubSub;
-use Google\Cloud\Iam\Iam;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Core\Iam\Iam;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
use Google\Cloud\PubSub\Connection\IamTopic;
use InvalidArgumentException;
@@ -28,11 +31,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\PubSub\PubSubClient;
*
- * $client = new ServiceBuilder();
- *
- * $pubsub = $client->pubsub();
+ * $pubsub = new PubSubClient();
* $topic = $pubsub->topic('my-new-topic');
* ```
*
@@ -43,6 +44,7 @@
*/
class Topic
{
+ use ArrayTrait;
use ResourceNameTrait;
/**
@@ -394,27 +396,30 @@ public function subscription($name)
* Configuration Options
*
* @type int $pageSize Maximum number of subscriptions to return.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function subscriptions(array $options = [])
{
- $options['pageToken'] = null;
-
- do {
- $response = $this->connection->listSubscriptionsByTopic($options + [
- 'topic' => $this->name
- ]);
-
- foreach ($response['subscriptions'] as $subscription) {
- yield $this->subscriptionFactory($subscription);
- }
-
- // If there's a page token, we'll request the next page.
- $options['pageToken'] = isset($response['nextPageToken'])
- ? $response['nextPageToken']
- : null;
- } while ($options['pageToken']);
+ $resultLimit = $this->pluck('resultLimit', $options, false);
+
+ return new ItemIterator(
+ new PageIterator(
+ function ($subscription) {
+ return $this->subscriptionFactory($subscription);
+ },
+ [$this->connection, 'listSubscriptionsByTopic'],
+ $options + ['topic' => $this->name],
+ [
+ 'itemsKey' => 'subscriptions',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
}
/**
diff --git a/src/PubSub/V1/PublisherClient.php b/src/PubSub/V1/PublisherClient.php
index 80131bea8db8..101156523195 100644
--- a/src/PubSub/V1/PublisherClient.php
+++ b/src/PubSub/V1/PublisherClient.php
@@ -1,6 +1,6 @@
null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
diff --git a/src/PubSub/V1/README.md b/src/PubSub/V1/README.md
new file mode 100644
index 000000000000..ff9c65ee047f
--- /dev/null
+++ b/src/PubSub/V1/README.md
@@ -0,0 +1,5 @@
+# Cloud Pub\Sub
+
+Cloud Pub/Sub is a fully-managed real-time messaging service that allows you to send and receive messages between independent applications.
+
+For more information, see [cloud.google.com](https://cloud.google.com/pubsub/).
diff --git a/src/PubSub/V1/SubscriberClient.php b/src/PubSub/V1/SubscriberClient.php
index abdab57f9b8a..52d055abbc34 100644
--- a/src/PubSub/V1/SubscriberClient.php
+++ b/src/PubSub/V1/SubscriberClient.php
@@ -1,6 +1,6 @@
render([
+ 'project' => $project,
+ 'snapshot' => $snapshot,
+ ]);
+ }
+
/**
* Formats a string containing the fully-qualified path to represent
* a subscription resource.
@@ -160,6 +182,24 @@ public static function parseProjectFromProjectName($projectName)
return self::getProjectNameTemplate()->match($projectName)['project'];
}
+ /**
+ * Parses the project from the given fully-qualified path which
+ * represents a snapshot resource.
+ */
+ public static function parseProjectFromSnapshotName($snapshotName)
+ {
+ return self::getSnapshotNameTemplate()->match($snapshotName)['project'];
+ }
+
+ /**
+ * Parses the snapshot from the given fully-qualified path which
+ * represents a snapshot resource.
+ */
+ public static function parseSnapshotFromSnapshotName($snapshotName)
+ {
+ return self::getSnapshotNameTemplate()->match($snapshotName)['snapshot'];
+ }
+
/**
* Parses the project from the given fully-qualified path which
* represents a subscription resource.
@@ -205,6 +245,15 @@ private static function getProjectNameTemplate()
return self::$projectNameTemplate;
}
+ private static function getSnapshotNameTemplate()
+ {
+ if (self::$snapshotNameTemplate == null) {
+ self::$snapshotNameTemplate = new PathTemplate('projects/{project}/snapshots/{snapshot}');
+ }
+
+ return self::$snapshotNameTemplate;
+ }
+
private static function getSubscriptionNameTemplate()
{
if (self::$subscriptionNameTemplate == null) {
@@ -232,14 +281,43 @@ private static function getPageStreamingDescriptors()
'responsePageTokenField' => 'next_page_token',
'resourceField' => 'subscriptions',
]);
+ $listSnapshotsPageStreamingDescriptor =
+ new PageStreamingDescriptor([
+ 'requestPageTokenField' => 'page_token',
+ 'requestPageSizeField' => 'page_size',
+ 'responsePageTokenField' => 'next_page_token',
+ 'resourceField' => 'snapshots',
+ ]);
$pageStreamingDescriptors = [
'listSubscriptions' => $listSubscriptionsPageStreamingDescriptor,
+ 'listSnapshots' => $listSnapshotsPageStreamingDescriptor,
];
return $pageStreamingDescriptors;
}
+ private static function getGrpcStreamingDescriptors()
+ {
+ return [
+ 'streamingPull' => [
+ 'grpcStreamingType' => 'BidiStreaming',
+ 'resourcesField' => 'getReceivedMessagesList',
+ ],
+ ];
+ }
+
+ private static function getGapicVersion()
+ {
+ if (file_exists(__DIR__.'/../VERSION')) {
+ return trim(file_get_contents(__DIR__.'/../VERSION'));
+ } elseif (class_exists('\Google\Cloud\ServiceBuilder')) {
+ return \Google\Cloud\ServiceBuilder::VERSION;
+ } else {
+ return;
+ }
+ }
+
// TODO(garrettjones): add channel (when supported in gRPC)
/**
* Constructor.
@@ -265,9 +343,6 @@ private static function getPageStreamingDescriptors()
* that don't use retries. For calls that use retries,
* set the timeout in RetryOptions.
* Default: 30000 (30 seconds)
- * @type string $appName The codename of the calling service. Default 'gax'.
- * @type string $appVersion The version of the calling service.
- * Default: the current version of GAX.
* @type \Google\Auth\CredentialsLoader $credentialsLoader
* A CredentialsLoader object created using the
* Google\Auth library.
@@ -284,30 +359,35 @@ public function __construct($options = [])
],
'retryingOverride' => null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
+
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
$this->descriptors = [
'createSubscription' => $defaultDescriptors,
'getSubscription' => $defaultDescriptors,
+ 'updateSubscription' => $defaultDescriptors,
'listSubscriptions' => $defaultDescriptors,
'deleteSubscription' => $defaultDescriptors,
'modifyAckDeadline' => $defaultDescriptors,
'acknowledge' => $defaultDescriptors,
'pull' => $defaultDescriptors,
+ 'streamingPull' => $defaultDescriptors,
'modifyPushConfig' => $defaultDescriptors,
+ 'listSnapshots' => $defaultDescriptors,
+ 'createSnapshot' => $defaultDescriptors,
+ 'deleteSnapshot' => $defaultDescriptors,
+ 'seek' => $defaultDescriptors,
'setIamPolicy' => $defaultDescriptors,
'getIamPolicy' => $defaultDescriptors,
'testIamPermissions' => $defaultDescriptors,
@@ -316,6 +396,10 @@ public function __construct($options = [])
foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) {
$this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor;
}
+ $grpcStreamingDescriptors = self::getGrpcStreamingDescriptors();
+ foreach ($grpcStreamingDescriptors as $method => $grpcStreamingDescriptor) {
+ $this->descriptors[$method]['grpcStreamingDescriptor'] = $grpcStreamingDescriptor;
+ }
$clientConfigJsonString = file_get_contents(__DIR__.'/resources/subscriber_client_config.json');
$clientConfig = json_decode($clientConfigJsonString, true);
@@ -424,6 +508,18 @@ public function __construct($options = [])
*
* If the subscriber never acknowledges the message, the Pub/Sub
* system will eventually redeliver the message.
+ * @type bool $retainAckedMessages
+ * Indicates whether to retain acknowledged messages. If true, then
+ * messages are not expunged from the subscription's backlog, even if they are
+ * acknowledged, until they fall out of the `message_retention_duration`
+ * window.
+ * @type Duration $messageRetentionDuration
+ * How long to retain unacknowledged messages in the subscription's backlog,
+ * from the moment a message is published.
+ * If `retain_acked_messages` is true, then this also configures the retention
+ * of acknowledged messages, and thus configures how far back in time a `Seek`
+ * can be done. Defaults to 7 days. Cannot be more than 7 days or less than 10
+ * minutes.
* @type \Google\GAX\RetrySettings $retrySettings
* Retry settings to use for this call. If present, then
* $timeoutMillis is ignored.
@@ -447,6 +543,12 @@ public function createSubscription($name, $topic, $optionalArgs = [])
if (isset($optionalArgs['ackDeadlineSeconds'])) {
$request->setAckDeadlineSeconds($optionalArgs['ackDeadlineSeconds']);
}
+ if (isset($optionalArgs['retainAckedMessages'])) {
+ $request->setRetainAckedMessages($optionalArgs['retainAckedMessages']);
+ }
+ if (isset($optionalArgs['messageRetentionDuration'])) {
+ $request->setMessageRetentionDuration($optionalArgs['messageRetentionDuration']);
+ }
$mergedSettings = $this->defaultCallSettings['createSubscription']->merge(
new CallSettings($optionalArgs)
@@ -516,6 +618,62 @@ public function getSubscription($subscription, $optionalArgs = [])
['call_credentials_callback' => $this->createCredentialsCallback()]);
}
+ /**
+ * Updates an existing subscription. Note that certain properties of a
+ * subscription, such as its topic, are not modifiable.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $subscriberClient = new SubscriberClient();
+ * $subscription = new Subscription();
+ * $updateMask = new FieldMask();
+ * $response = $subscriberClient->updateSubscription($subscription, $updateMask);
+ * } finally {
+ * $subscriberClient->close();
+ * }
+ * ```
+ *
+ * @param Subscription $subscription The updated subscription object.
+ * @param FieldMask $updateMask Indicates which fields in the provided subscription to update.
+ * Must be specified and non-empty.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\pubsub\v1\Subscription
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function updateSubscription($subscription, $updateMask, $optionalArgs = [])
+ {
+ $request = new UpdateSubscriptionRequest();
+ $request->setSubscription($subscription);
+ $request->setUpdateMask($updateMask);
+
+ $mergedSettings = $this->defaultCallSettings['updateSubscription']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->subscriberStub,
+ 'UpdateSubscription',
+ $mergedSettings,
+ $this->descriptors['updateSubscription']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
/**
* Lists matching subscriptions.
*
@@ -848,6 +1006,89 @@ public function pull($subscription, $maxMessages, $optionalArgs = [])
['call_credentials_callback' => $this->createCredentialsCallback()]);
}
+ /**
+ * (EXPERIMENTAL) StreamingPull is an experimental feature. This RPC will
+ * respond with UNIMPLEMENTED errors unless you have been invited to test
+ * this feature. Contact cloud-pubsub@google.com with any questions.
+ *
+ * Establishes a stream with the server, which sends messages down to the
+ * client. The client streams acknowledgements and ack deadline modifications
+ * back to the server. The server will close the stream and return the status
+ * on any error. The server may close the stream with status `OK` to reassign
+ * server-side resources, in which case, the client should re-establish the
+ * stream. `UNAVAILABLE` may also be returned in the case of a transient error
+ * (e.g., a server restart). These should also be retried by the client. Flow
+ * control can be achieved by configuring the underlying RPC channel.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $subscriberClient = new SubscriberClient();
+ * $formattedSubscription = SubscriberClient::formatSubscriptionName("[PROJECT]", "[SUBSCRIPTION]");
+ * $streamAckDeadlineSeconds = 0;
+ * $request = new StreamingPullRequest();
+ * $request->setSubscription($formattedSubscription);
+ * $request->setStreamAckDeadlineSeconds($streamAckDeadlineSeconds);
+ * $requests = [$request];
+ *
+ * // Write all requests to the server, then read all responses until the
+ * // stream is complete
+ * $stream = $subscriberClient->streamingPull();
+ * $stream->writeAll($requests);
+ * foreach ($stream->closeWriteAndReadAll() as $element) {
+ * // doSomethingWith($element);
+ * }
+ *
+ * // OR write requests individually, making read() calls if
+ * // required. Call closeWrite() once writes are complete, and read the
+ * // remaining responses from the server.
+ * $stream = $subscriberClient->streamingPull();
+ * foreach ($requests as $request) {
+ * $stream->write($request);
+ * // if required, read a single response from the stream
+ * $element = $stream->read();
+ * // doSomethingWith($element)
+ * }
+ * $stream->closeWrite();
+ * $element = $stream->read();
+ * while (!is_null($element)) {
+ * // doSomethingWith($element)
+ * $element = $stream->read();
+ * }
+ * } finally {
+ * $subscriberClient->close();
+ * }
+ * ```
+ *
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type int $timeoutMillis
+ * Timeout to use for this call.
+ * }
+ *
+ * @return \Google\GAX\BidiStreamingResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function streamingPull($optionalArgs = [])
+ {
+ $mergedSettings = $this->defaultCallSettings['streamingPull']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->subscriberStub,
+ 'StreamingPull',
+ $mergedSettings,
+ $this->descriptors['streamingPull']
+ );
+
+ return $callable(
+ null,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
/**
* Modifies the `PushConfig` for a specified subscription.
*
@@ -911,6 +1152,287 @@ public function modifyPushConfig($subscription, $pushConfig, $optionalArgs = [])
['call_credentials_callback' => $this->createCredentialsCallback()]);
}
+ /**
+ * Lists the existing snapshots.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $subscriberClient = new SubscriberClient();
+ * $formattedProject = SubscriberClient::formatProjectName("[PROJECT]");
+ * // Iterate through all elements
+ * $pagedResponse = $subscriberClient->listSnapshots($formattedProject);
+ * foreach ($pagedResponse->iterateAllElements() as $element) {
+ * // doSomethingWith($element);
+ * }
+ *
+ * // OR iterate over pages of elements, with the maximum page size set to 5
+ * $pagedResponse = $subscriberClient->listSnapshots($formattedProject, ['pageSize' => 5]);
+ * foreach ($pagedResponse->iteratePages() as $page) {
+ * foreach ($page as $element) {
+ * // doSomethingWith($element);
+ * }
+ * }
+ * } finally {
+ * $subscriberClient->close();
+ * }
+ * ```
+ *
+ * @param string $project The name of the cloud project that snapshots belong to.
+ * Format is `projects/{project}`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type int $pageSize
+ * The maximum number of resources contained in the underlying API
+ * response. The API may return fewer values in a page, even if
+ * there are additional values to be retrieved.
+ * @type string $pageToken
+ * A page token is used to specify a page of values to be returned.
+ * If no page token is specified (the default), the first page
+ * of values will be returned. Any page token used here must have
+ * been generated by a previous call to the API.
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \Google\GAX\PagedListResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function listSnapshots($project, $optionalArgs = [])
+ {
+ $request = new ListSnapshotsRequest();
+ $request->setProject($project);
+ if (isset($optionalArgs['pageSize'])) {
+ $request->setPageSize($optionalArgs['pageSize']);
+ }
+ if (isset($optionalArgs['pageToken'])) {
+ $request->setPageToken($optionalArgs['pageToken']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['listSnapshots']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->subscriberStub,
+ 'ListSnapshots',
+ $mergedSettings,
+ $this->descriptors['listSnapshots']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Creates a snapshot from the requested subscription.
+ * If the snapshot already exists, returns `ALREADY_EXISTS`.
+ * If the requested subscription doesn't exist, returns `NOT_FOUND`.
+ *
+ * If the name is not provided in the request, the server will assign a random
+ * name for this snapshot on the same project as the subscription, conforming
+ * to the
+ * [resource name format](https://cloud.google.com/pubsub/docs/overview#names).
+ * The generated name is populated in the returned Snapshot object.
+ * Note that for REST API requests, you must specify a name in the request.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $subscriberClient = new SubscriberClient();
+ * $formattedName = SubscriberClient::formatSnapshotName("[PROJECT]", "[SNAPSHOT]");
+ * $formattedSubscription = SubscriberClient::formatSubscriptionName("[PROJECT]", "[SUBSCRIPTION]");
+ * $response = $subscriberClient->createSnapshot($formattedName, $formattedSubscription);
+ * } finally {
+ * $subscriberClient->close();
+ * }
+ * ```
+ *
+ * @param string $name Optional user-provided name for this snapshot.
+ * If the name is not provided in the request, the server will assign a random
+ * name for this snapshot on the same project as the subscription.
+ * Note that for REST API requests, you must specify a name.
+ * Format is `projects/{project}/snapshots/{snap}`.
+ * @param string $subscription The subscription whose backlog the snapshot retains.
+ * Specifically, the created snapshot is guaranteed to retain:
+ * (a) The existing backlog on the subscription. More precisely, this is
+ * defined as the messages in the subscription's backlog that are
+ * unacknowledged upon the successful completion of the
+ * `CreateSnapshot` request; as well as:
+ * (b) Any messages published to the subscription's topic following the
+ * successful completion of the CreateSnapshot request.
+ * Format is `projects/{project}/subscriptions/{sub}`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\pubsub\v1\Snapshot
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function createSnapshot($name, $subscription, $optionalArgs = [])
+ {
+ $request = new CreateSnapshotRequest();
+ $request->setName($name);
+ $request->setSubscription($subscription);
+
+ $mergedSettings = $this->defaultCallSettings['createSnapshot']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->subscriberStub,
+ 'CreateSnapshot',
+ $mergedSettings,
+ $this->descriptors['createSnapshot']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Removes an existing snapshot. All messages retained in the snapshot
+ * are immediately dropped. After a snapshot is deleted, a new one may be
+ * created with the same name, but the new one has no association with the old
+ * snapshot or its subscription, unless the same subscription is specified.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $subscriberClient = new SubscriberClient();
+ * $formattedSnapshot = SubscriberClient::formatSnapshotName("[PROJECT]", "[SNAPSHOT]");
+ * $subscriberClient->deleteSnapshot($formattedSnapshot);
+ * } finally {
+ * $subscriberClient->close();
+ * }
+ * ```
+ *
+ * @param string $snapshot The name of the snapshot to delete.
+ * Format is `projects/{project}/snapshots/{snap}`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function deleteSnapshot($snapshot, $optionalArgs = [])
+ {
+ $request = new DeleteSnapshotRequest();
+ $request->setSnapshot($snapshot);
+
+ $mergedSettings = $this->defaultCallSettings['deleteSnapshot']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->subscriberStub,
+ 'DeleteSnapshot',
+ $mergedSettings,
+ $this->descriptors['deleteSnapshot']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Seeks an existing subscription to a point in time or to a given snapshot,
+ * whichever is provided in the request.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $subscriberClient = new SubscriberClient();
+ * $formattedSubscription = SubscriberClient::formatSubscriptionName("[PROJECT]", "[SUBSCRIPTION]");
+ * $response = $subscriberClient->seek($formattedSubscription);
+ * } finally {
+ * $subscriberClient->close();
+ * }
+ * ```
+ *
+ * @param string $subscription The subscription to affect.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type Timestamp $time
+ * The time to seek to.
+ * Messages retained in the subscription that were published before this
+ * time are marked as acknowledged, and messages retained in the
+ * subscription that were published after this time are marked as
+ * unacknowledged. Note that this operation affects only those messages
+ * retained in the subscription (configured by the combination of
+ * `message_retention_duration` and `retain_acked_messages`). For example,
+ * if `time` corresponds to a point before the message retention
+ * window (or to a point before the system's notion of the subscription
+ * creation time), only retained messages will be marked as unacknowledged,
+ * and already-expunged messages will not be restored.
+ * @type string $snapshot
+ * The snapshot to seek to. The snapshot's topic must be the same as that of
+ * the provided subscription.
+ * Format is `projects/{project}/snapshots/{snap}`.
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\pubsub\v1\SeekResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function seek($subscription, $optionalArgs = [])
+ {
+ $request = new SeekRequest();
+ $request->setSubscription($subscription);
+ if (isset($optionalArgs['time'])) {
+ $request->setTime($optionalArgs['time']);
+ }
+ if (isset($optionalArgs['snapshot'])) {
+ $request->setSnapshot($optionalArgs['snapshot']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['seek']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->subscriberStub,
+ 'Seek',
+ $mergedSettings,
+ $this->descriptors['seek']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
/**
* Sets the access control policy on the specified resource. Replaces any
* existing policy.
diff --git a/src/PubSub/V1/resources/publisher_client_config.json b/src/PubSub/V1/resources/publisher_client_config.json
index 4df74e5170e0..99ebf2412f29 100644
--- a/src/PubSub/V1/resources/publisher_client_config.json
+++ b/src/PubSub/V1/resources/publisher_client_config.json
@@ -2,17 +2,17 @@
"interfaces": {
"google.pubsub.v1.Publisher": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "one_plus_delivery": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "one_plus_delivery": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
diff --git a/src/PubSub/V1/resources/subscriber_client_config.json b/src/PubSub/V1/resources/subscriber_client_config.json
index c39ebcb51e63..8fbb5fb13631 100644
--- a/src/PubSub/V1/resources/subscriber_client_config.json
+++ b/src/PubSub/V1/resources/subscriber_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.pubsub.v1.Subscriber": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
@@ -41,6 +41,11 @@
"retry_codes_name": "idempotent",
"retry_params_name": "default"
},
+ "UpdateSubscription": {
+ "timeout_millis": 60000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
"ListSubscriptions": {
"timeout_millis": 60000,
"retry_codes_name": "idempotent",
@@ -66,11 +71,36 @@
"retry_codes_name": "non_idempotent",
"retry_params_name": "messaging"
},
+ "StreamingPull": {
+ "timeout_millis": 60000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "messaging"
+ },
"ModifyPushConfig": {
"timeout_millis": 60000,
"retry_codes_name": "non_idempotent",
"retry_params_name": "default"
},
+ "ListSnapshots": {
+ "timeout_millis": 60000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "CreateSnapshot": {
+ "timeout_millis": 60000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "DeleteSnapshot": {
+ "timeout_millis": 60000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "Seek": {
+ "timeout_millis": 60000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
"SetIamPolicy": {
"timeout_millis": 60000,
"retry_codes_name": "non_idempotent",
diff --git a/src/PubSub/VERSION b/src/PubSub/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/PubSub/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/PubSub/composer.json b/src/PubSub/composer.json
new file mode 100644
index 000000000000..59ab2de6c647
--- /dev/null
+++ b/src/PubSub/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "google/cloud-pubsub",
+ "description": "Cloud PubSub Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "suggest": {
+ "google/gax": "Required to support gRPC",
+ "google/proto-client-php": "Required to support gRPC"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-pubsub",
+ "target": "GoogleCloudPlatform/google-cloud-php-pubsub.git",
+ "path": "src/PubSub",
+ "entry": "PubSubClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\PubSub\\": ""
+ }
+ }
+}
diff --git a/src/Retry.php b/src/Retry.php
new file mode 100644
index 000000000000..94167fb2b6fa
--- /dev/null
+++ b/src/Retry.php
@@ -0,0 +1,106 @@
+ (int >= 0), 'nanos' => (int >= 0)] specifying how
+ * long an operation should pause before retrying. Should accept a
+ * single argument of type `\Exception`.
+ * @param callable $retryFunction [optional] returns bool for whether or not
+ * to retry.
+ */
+ public function __construct(
+ $retries,
+ callable $delayFunction,
+ callable $retryFunction = null
+ ) {
+ $this->retries = $retries !== null ? (int) $retries : 3;
+ $this->retryFunction = $retryFunction;
+ $this->delayFunction = $delayFunction;
+ }
+
+ /**
+ * Executes the retry process.
+ *
+ * @param callable $function
+ * @param array $arguments [optional]
+ * @return mixed
+ * @throws \Exception The last exception caught while retrying.
+ */
+ public function execute(callable $function, array $arguments = [])
+ {
+ $delayFunction = $this->delayFunction;
+ $retryAttempt = 0;
+ $exception = null;
+
+ while (true) {
+ try {
+ return call_user_func_array($function, $arguments);
+ } catch (\Exception $exception) {
+ if ($this->retryFunction) {
+ if (!call_user_func($this->retryFunction, $exception)) {
+ throw $exception;
+ }
+ }
+
+ if ($retryAttempt >= $this->retries) {
+ break;
+ }
+
+ $delayFunction($exception);
+ $retryAttempt++;
+ }
+ }
+
+ throw $exception;
+ }
+
+ /**
+ * @param callable $delayFunction
+ * @return void
+ */
+ public function setDelayFunction(callable $delayFunction)
+ {
+ $this->delayFunction = $delayFunction;
+ }
+}
diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php
index d8df7682795c..5c06f99d9489 100644
--- a/src/ServiceBuilder.php
+++ b/src/ServiceBuilder.php
@@ -23,6 +23,7 @@
use Google\Cloud\Logging\LoggingClient;
use Google\Cloud\NaturalLanguage\NaturalLanguageClient;
use Google\Cloud\PubSub\PubSubClient;
+use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Speech\SpeechClient;
use Google\Cloud\Storage\StorageClient;
use Google\Cloud\Translate\TranslateClient;
@@ -48,7 +49,7 @@
*/
class ServiceBuilder
{
- const VERSION = '0.20.2';
+ const VERSION = '0.24.0';
/**
* @var array Configuration options to be used between clients.
@@ -97,8 +98,8 @@ public function __construct(array $config = [])
}
/**
- * Google Cloud BigQuery client. Allows you to create, manage, share and query
- * data. Find more information at
+ * Google Cloud BigQuery allows you to create, manage, share and query
+ * data. Find more information at the
* [Google Cloud BigQuery Docs](https://cloud.google.com/bigquery/what-is-bigquery).
*
* Example:
@@ -111,8 +112,9 @@ public function __construct(array $config = [])
* {@see Google\Cloud\ServiceBuilder::__construct()} for the other available options.
*
* @type bool $returnInt64AsObject If true, 64 bit integers will be
- * returned as a {@see Google\Cloud\Int64} object for 32 bit
+ * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
+ * }
* @return BigQueryClient
*/
public function bigQuery(array $config = [])
@@ -121,8 +123,8 @@ public function bigQuery(array $config = [])
}
/**
- * Google Cloud Datastore client. Cloud Datastore is a highly-scalable NoSQL
- * database for your applications. Find more information at
+ * Google Cloud Datastore is a highly-scalable NoSQL database for your
+ * applications. Find more information at the
* [Google Cloud Datastore docs](https://cloud.google.com/datastore/docs/).
*
* Example:
@@ -135,7 +137,7 @@ public function bigQuery(array $config = [])
* {@see Google\Cloud\ServiceBuilder::__construct()} for the other available options.
*
* @type bool $returnInt64AsObject If true, 64 bit integers will be
- * returned as a {@see Google\Cloud\Int64} object for 32 bit
+ * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
* @return DatastoreClient
*/
@@ -145,9 +147,9 @@ public function datastore(array $config = [])
}
/**
- * Google Stackdriver Logging client. Allows you to store, search, analyze,
- * monitor, and alert on log data and events from Google Cloud Platform and
- * Amazon Web Services. Find more information at
+ * Google Stackdriver Logging allows you to store, search, analyze, monitor,
+ * and alert on log data and events from Google Cloud Platform and Amazon
+ * Web Services. Find more information at the
* [Google Stackdriver Logging docs](https://cloud.google.com/logging/docs/).
*
* Example:
@@ -165,10 +167,10 @@ public function logging(array $config = [])
}
/**
- * Google Cloud Natural Language client. Provides natural language
- * understanding technologies to developers, including sentiment analysis,
- * entity recognition, and syntax analysis. Currently only English, Spanish,
- * and Japanese textual context are supported. Find more information at
+ * Google Cloud Natural Language provides natural language understanding
+ * technologies to developers, including sentiment analysis, entity
+ * recognition, and syntax analysis. Currently only English, Spanish,
+ * and Japanese textual context are supported. Find more information at the
* [Google Cloud Natural Language docs](https://cloud.google.com/natural-language/docs/).
*
* Example:
@@ -186,8 +188,8 @@ public function naturalLanguage(array $config = [])
}
/**
- * Google Cloud Pub/Sub client. Allows you to send and receive
- * messages between independent applications. Find more information at
+ * Google Cloud Pub/Sub allows you to send and receive messages between
+ * independent applications. Find more information at the
* [Google Cloud Pub/Sub docs](https://cloud.google.com/pubsub/docs/).
*
* Example:
@@ -210,11 +212,35 @@ public function pubsub(array $config = [])
}
/**
- * Google Cloud Speech client. Enables easy integration of Google speech
- * recognition technologies into developer applications. Send audio and
- * receive a text transcription from the Cloud Speech API service. Find more
- * information at
- * [Google Cloud Speech API docs](https://developers.google.com/speech).
+ * Google Cloud Spanner is a highly scalable, transactional, managed, NewSQL
+ * database service. Find more information at
+ * [Google Cloud Spanner API docs](https://cloud.google.com/spanner/).
+ *
+ * Example:
+ * ```
+ * $spanner = $cloud->spanner();
+ * ```
+ *
+ * @param array $config [optional] {
+ * Configuration options. See
+ * {@see Google\Cloud\ServiceBuilder::__construct()} for the other available options.
+ *
+ * @type bool $returnInt64AsObject If true, 64 bit integers will be
+ * returned as a {@see Google\Cloud\Int64} object for 32 bit
+ * platform compatibility. **Defaults to** false.
+ * }
+ * @return SpannerClient
+ */
+ public function spanner(array $config = [])
+ {
+ return new SpannerClient($config ? $this->resolveConfig($config) : $this->config);
+ }
+
+ /**
+ * Google Cloud Speech enables easy integration of Google speech recognition
+ * technologies into developer applications. Send audio and receive a text
+ * transcription from the Cloud Speech API service. Find more information at
+ * the [Google Cloud Speech API docs](https://cloud.google.com/speech/docs/).
*
* Example:
* ```
@@ -231,8 +257,8 @@ public function speech(array $config = [])
}
/**
- * Google Cloud Storage client. Allows you to store and retrieve data on
- * Google's infrastructure. Find more information at
+ * Google Cloud Storage allows you to store and retrieve data on Google's
+ * infrastructure. Find more information at the
* [Google Cloud Storage API docs](https://developers.google.com/storage).
*
* Example:
@@ -250,9 +276,10 @@ public function storage(array $config = [])
}
/**
- * Google Cloud Vision client. Allows you to understand the content of an
- * image, classify images into categories, detect text, objects, faces and
- * more. Find more information at [Google Cloud Vision docs](https://cloud.google.com/vision/docs/).
+ * Google Cloud Vision allows you to understand the content of an image,
+ * classify images into categories, detect text, objects, faces and more.
+ * Find more information at the
+ * [Google Cloud Vision docs](https://cloud.google.com/vision/docs/).
*
* Example:
* ```
@@ -269,13 +296,14 @@ public function vision(array $config = [])
}
/**
- * Google Translate client. Provides the ability to dynamically
- * translate text between thousands of language pairs and lets websites and
- * programs integrate with the Google Cloud Translation API
- * programmatically. The Google Cloud Translation API is available as a paid
+ * Google Cloud Translation provides the ability to dynamically translate
+ * text between thousands of language pairs and lets websites and programs
+ * integrate with translation service programmatically.
+ *
+ * The Google Cloud Translation API is available as a paid
* service. See the [Pricing](https://cloud.google.com/translation/v2/pricing)
* and [FAQ](https://cloud.google.com/translation/v2/faq) pages for details.
- * Find more information at the
+ * Find more information at the the
* [Google Cloud Translation docs](https://cloud.google.com/translation/docs/).
*
* Please note that while the Google Cloud Translation API supports
diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php
new file mode 100644
index 000000000000..83284ea46f2c
--- /dev/null
+++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php
@@ -0,0 +1,1049 @@
+listDatabases($formattedParent);
+ * foreach ($pagedResponse->iterateAllElements() as $element) {
+ * // doSomethingWith($element);
+ * }
+ *
+ * // OR iterate over pages of elements, with the maximum page size set to 5
+ * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent, ['pageSize' => 5]);
+ * foreach ($pagedResponse->iteratePages() as $page) {
+ * foreach ($page as $element) {
+ * // doSomethingWith($element);
+ * }
+ * }
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * Many parameters require resource names to be formatted in a particular way. To assist
+ * with these names, this class includes a format method for each type of name, and additionally
+ * a parse method to extract the individual identifiers contained within names that are
+ * returned.
+ */
+class DatabaseAdminClient
+{
+ /**
+ * The default address of the service.
+ */
+ const SERVICE_ADDRESS = 'spanner.googleapis.com';
+
+ /**
+ * The default port of the service.
+ */
+ const DEFAULT_SERVICE_PORT = 443;
+
+ /**
+ * The default timeout for non-retrying methods.
+ */
+ const DEFAULT_TIMEOUT_MILLIS = 30000;
+
+ /**
+ * The name of the code generator, to be included in the agent header.
+ */
+ const CODEGEN_NAME = 'gapic';
+
+ /**
+ * The code generator version, to be included in the agent header.
+ */
+ const CODEGEN_VERSION = '0.1.0';
+
+ private static $instanceNameTemplate;
+ private static $databaseNameTemplate;
+
+ private $grpcCredentialsHelper;
+ private $databaseAdminStub;
+ private $scopes;
+ private $defaultCallSettings;
+ private $descriptors;
+ private $operationsClient;
+
+ /**
+ * Formats a string containing the fully-qualified path to represent
+ * a instance resource.
+ */
+ public static function formatInstanceName($project, $instance)
+ {
+ return self::getInstanceNameTemplate()->render([
+ 'project' => $project,
+ 'instance' => $instance,
+ ]);
+ }
+
+ /**
+ * Formats a string containing the fully-qualified path to represent
+ * a database resource.
+ */
+ public static function formatDatabaseName($project, $instance, $database)
+ {
+ return self::getDatabaseNameTemplate()->render([
+ 'project' => $project,
+ 'instance' => $instance,
+ 'database' => $database,
+ ]);
+ }
+
+ /**
+ * Parses the project from the given fully-qualified path which
+ * represents a instance resource.
+ */
+ public static function parseProjectFromInstanceName($instanceName)
+ {
+ return self::getInstanceNameTemplate()->match($instanceName)['project'];
+ }
+
+ /**
+ * Parses the instance from the given fully-qualified path which
+ * represents a instance resource.
+ */
+ public static function parseInstanceFromInstanceName($instanceName)
+ {
+ return self::getInstanceNameTemplate()->match($instanceName)['instance'];
+ }
+
+ /**
+ * Parses the project from the given fully-qualified path which
+ * represents a database resource.
+ */
+ public static function parseProjectFromDatabaseName($databaseName)
+ {
+ return self::getDatabaseNameTemplate()->match($databaseName)['project'];
+ }
+
+ /**
+ * Parses the instance from the given fully-qualified path which
+ * represents a database resource.
+ */
+ public static function parseInstanceFromDatabaseName($databaseName)
+ {
+ return self::getDatabaseNameTemplate()->match($databaseName)['instance'];
+ }
+
+ /**
+ * Parses the database from the given fully-qualified path which
+ * represents a database resource.
+ */
+ public static function parseDatabaseFromDatabaseName($databaseName)
+ {
+ return self::getDatabaseNameTemplate()->match($databaseName)['database'];
+ }
+
+ private static function getInstanceNameTemplate()
+ {
+ if (self::$instanceNameTemplate == null) {
+ self::$instanceNameTemplate = new PathTemplate('projects/{project}/instances/{instance}');
+ }
+
+ return self::$instanceNameTemplate;
+ }
+
+ private static function getDatabaseNameTemplate()
+ {
+ if (self::$databaseNameTemplate == null) {
+ self::$databaseNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}');
+ }
+
+ return self::$databaseNameTemplate;
+ }
+
+ private static function getPageStreamingDescriptors()
+ {
+ $listDatabasesPageStreamingDescriptor =
+ new PageStreamingDescriptor([
+ 'requestPageTokenField' => 'page_token',
+ 'requestPageSizeField' => 'page_size',
+ 'responsePageTokenField' => 'next_page_token',
+ 'resourceField' => 'databases',
+ ]);
+
+ $pageStreamingDescriptors = [
+ 'listDatabases' => $listDatabasesPageStreamingDescriptor,
+ ];
+
+ return $pageStreamingDescriptors;
+ }
+
+ public static function getLongRunningDescriptors()
+ {
+ return [
+ 'createDatabase' => [
+ 'operationReturnType' => '\google\spanner\admin\database\v1\Database',
+ 'metadataReturnType' => '\google\spanner\admin\database\v1\CreateDatabaseMetadata',
+ ],
+ 'updateDatabaseDdl' => [
+ 'operationReturnType' => '\google\protobuf\EmptyC',
+ 'metadataReturnType' => '\google\spanner\admin\database\v1\UpdateDatabaseDdlMetadata',
+ ],
+ ];
+ }
+
+ /**
+ * Return an OperationsClient object with the same endpoint as $this.
+ *
+ * @return \Google\GAX\LongRunning\OperationsClient
+ */
+ public function getOperationsClient()
+ {
+ return $this->operationsClient;
+ }
+
+ /**
+ * Resume an existing long running operation that was previously started
+ * by a long running API method. If $methodName is not provided, or does
+ * not match a long running API method, then the operation can still be
+ * resumed, but the OperationResponse object will not deserialize the
+ * final response.
+ *
+ * @param string $operationName The name of the long running operation
+ * @param string $methodName The name of the method used to start the operation
+ *
+ * @return \Google\GAX\OperationResponse
+ */
+ public function resumeOperation($operationName, $methodName = null)
+ {
+ $lroDescriptors = self::getLongRunningDescriptors();
+ if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) {
+ $options = $lroDescriptors[$methodName];
+ } else {
+ $options = [];
+ }
+ $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options);
+ $operation->reload();
+
+ return $operation;
+ }
+
+ // TODO(garrettjones): add channel (when supported in gRPC)
+ /**
+ * Constructor.
+ *
+ * @param array $options {
+ * Optional. Options for configuring the service API wrapper.
+ *
+ * @type string $serviceAddress The domain name of the API remote host.
+ * Default 'spanner.googleapis.com'.
+ * @type mixed $port The port on which to connect to the remote host. Default 443.
+ * @type \Grpc\ChannelCredentials $sslCreds
+ * A `ChannelCredentials` for use with an SSL-enabled channel.
+ * Default: a credentials object returned from
+ * \Grpc\ChannelCredentials::createSsl()
+ * @type array $scopes A string array of scopes to use when acquiring credentials.
+ * Default the scopes for the Google Cloud Spanner Database Admin API.
+ * @type array $retryingOverride
+ * An associative array of string => RetryOptions, where the keys
+ * are method names (e.g. 'createFoo'), that overrides default retrying
+ * settings. A value of null indicates that the method in question should
+ * not retry.
+ * @type int $timeoutMillis The timeout in milliseconds to use for calls
+ * that don't use retries. For calls that use retries,
+ * set the timeout in RetryOptions.
+ * Default: 30000 (30 seconds)
+ * @type string $appName The codename of the calling service. Default 'gax'.
+ * @type string $appVersion The version of the calling service.
+ * Default: the current version of GAX.
+ * @type \Google\Auth\CredentialsLoader $credentialsLoader
+ * A CredentialsLoader object created using the
+ * Google\Auth library.
+ * }
+ */
+ public function __construct($options = [])
+ {
+ $defaultOptions = [
+ 'serviceAddress' => self::SERVICE_ADDRESS,
+ 'port' => self::DEFAULT_SERVICE_PORT,
+ 'scopes' => [
+ 'https://www.googleapis.com/auth/cloud-platform',
+ 'https://www.googleapis.com/auth/spanner.admin',
+ ],
+ 'retryingOverride' => null,
+ 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
+ 'appName' => 'gax',
+ 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ ];
+ $options = array_merge($defaultOptions, $options);
+
+ if (array_key_exists('operationsClient', $options)) {
+ $this->operationsClient = $options['operationsClient'];
+ } else {
+ $this->operationsClient = new OperationsClient([
+ 'serviceAddress' => $options['serviceAddress'],
+ 'scopes' => $options['scopes'],
+ ]);
+ }
+
+ $headerDescriptor = new AgentHeaderDescriptor([
+ 'clientName' => $options['appName'],
+ 'clientVersion' => $options['appVersion'],
+ 'codeGenName' => self::CODEGEN_NAME,
+ 'codeGenVersion' => self::CODEGEN_VERSION,
+ 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'phpVersion' => phpversion(),
+ ]);
+
+ $defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
+ $this->descriptors = [
+ 'listDatabases' => $defaultDescriptors,
+ 'createDatabase' => $defaultDescriptors,
+ 'getDatabase' => $defaultDescriptors,
+ 'updateDatabaseDdl' => $defaultDescriptors,
+ 'dropDatabase' => $defaultDescriptors,
+ 'getDatabaseDdl' => $defaultDescriptors,
+ 'setIamPolicy' => $defaultDescriptors,
+ 'getIamPolicy' => $defaultDescriptors,
+ 'testIamPermissions' => $defaultDescriptors,
+ ];
+ $pageStreamingDescriptors = self::getPageStreamingDescriptors();
+ foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) {
+ $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor;
+ }
+ $longRunningDescriptors = self::getLongRunningDescriptors();
+ foreach ($longRunningDescriptors as $method => $longRunningDescriptor) {
+ $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient];
+ }
+
+ $clientConfigJsonString = file_get_contents(__DIR__.'/resources/database_admin_client_config.json');
+ $clientConfig = json_decode($clientConfigJsonString, true);
+ $this->defaultCallSettings =
+ CallSettings::load(
+ 'google.spanner.admin.database.v1.DatabaseAdmin',
+ $clientConfig,
+ $options['retryingOverride'],
+ GrpcConstants::getStatusCodeNames(),
+ $options['timeoutMillis']
+ );
+
+ $this->scopes = $options['scopes'];
+
+ $createStubOptions = [];
+ if (array_key_exists('sslCreds', $options)) {
+ $createStubOptions['sslCreds'] = $options['sslCreds'];
+ }
+ $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions);
+ $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions);
+
+ $createDatabaseAdminStubFunction = function ($hostname, $opts) {
+ return new DatabaseAdminGrpcClient($hostname, $opts);
+ };
+ if (array_key_exists('createDatabaseAdminStubFunction', $options)) {
+ $createDatabaseAdminStubFunction = $options['createDatabaseAdminStubFunction'];
+ }
+ $this->databaseAdminStub = $this->grpcCredentialsHelper->createStub(
+ $createDatabaseAdminStubFunction,
+ $options['serviceAddress'],
+ $options['port'],
+ $createStubOptions
+ );
+ }
+
+ /**
+ * Lists Cloud Spanner databases.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]");
+ * // Iterate through all elements
+ * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent);
+ * foreach ($pagedResponse->iterateAllElements() as $element) {
+ * // doSomethingWith($element);
+ * }
+ *
+ * // OR iterate over pages of elements, with the maximum page size set to 5
+ * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent, ['pageSize' => 5]);
+ * foreach ($pagedResponse->iteratePages() as $page) {
+ * foreach ($page as $element) {
+ * // doSomethingWith($element);
+ * }
+ * }
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $parent Required. The instance whose databases should be listed.
+ * Values are of the form `projects//instances/`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type int $pageSize
+ * The maximum number of resources contained in the underlying API
+ * response. The API may return fewer values in a page, even if
+ * there are additional values to be retrieved.
+ * @type string $pageToken
+ * A page token is used to specify a page of values to be returned.
+ * If no page token is specified (the default), the first page
+ * of values will be returned. Any page token used here must have
+ * been generated by a previous call to the API.
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \Google\GAX\PagedListResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function listDatabases($parent, $optionalArgs = [])
+ {
+ $request = new ListDatabasesRequest();
+ $request->setParent($parent);
+ if (isset($optionalArgs['pageSize'])) {
+ $request->setPageSize($optionalArgs['pageSize']);
+ }
+ if (isset($optionalArgs['pageToken'])) {
+ $request->setPageToken($optionalArgs['pageToken']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['listDatabases']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'ListDatabases',
+ $mergedSettings,
+ $this->descriptors['listDatabases']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Creates a new Cloud Spanner database and starts to prepare it for serving.
+ * The returned [long-running operation][google.longrunning.Operation] will
+ * have a name of the format `/operations/` and
+ * can be used to track preparation of the database. The
+ * [metadata][google.longrunning.Operation.metadata] field type is
+ * [CreateDatabaseMetadata][google.spanner.admin.database.v1.CreateDatabaseMetadata]. The
+ * [response][google.longrunning.Operation.response] field type is
+ * [Database][google.spanner.admin.database.v1.Database], if successful.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]");
+ * $createStatement = "";
+ * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement);
+ * $operationResponse->pollUntilComplete();
+ * if ($operationResponse->operationSucceeded()) {
+ * $result = $operationResponse->getResult();
+ * // doSomethingWith($result)
+ * } else {
+ * $error = $operationResponse->getError();
+ * // handleError($error)
+ * }
+ *
+ * // OR start the operation, keep the operation name, and resume later
+ * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement);
+ * $operationName = $operationResponse->getName();
+ * // ... do other work
+ * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'createDatabase');
+ * while (!$newOperationResponse->isDone()) {
+ * // ... do other work
+ * $newOperationResponse->reload();
+ * }
+ * if ($newOperationResponse->operationSucceeded()) {
+ * $result = $newOperationResponse->getResult();
+ * // doSomethingWith($result)
+ * } else {
+ * $error = $newOperationResponse->getError();
+ * // handleError($error)
+ * }
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $parent Required. The name of the instance that will serve the new database.
+ * Values are of the form `projects//instances/`.
+ * @param string $createStatement Required. A `CREATE DATABASE` statement, which specifies the ID of the
+ * new database. The database ID must conform to the regular expression
+ * `[a-z][a-z0-9_\-]*[a-z0-9]` and be between 2 and 30 characters in length.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type string[] $extraStatements
+ * An optional list of DDL statements to run inside the newly created
+ * database. Statements can create tables, indexes, etc. These
+ * statements execute atomically with the creation of the database:
+ * if there is an error in any statement, the database is not created.
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\longrunning\Operation
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function createDatabase($parent, $createStatement, $optionalArgs = [])
+ {
+ $request = new CreateDatabaseRequest();
+ $request->setParent($parent);
+ $request->setCreateStatement($createStatement);
+ if (isset($optionalArgs['extraStatements'])) {
+ foreach ($optionalArgs['extraStatements'] as $elem) {
+ $request->addExtraStatements($elem);
+ }
+ }
+
+ $mergedSettings = $this->defaultCallSettings['createDatabase']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'CreateDatabase',
+ $mergedSettings,
+ $this->descriptors['createDatabase']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Gets the state of a Cloud Spanner database.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedName = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+ * $response = $databaseAdminClient->getDatabase($formattedName);
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $name Required. The name of the requested database. Values are of the form
+ * `projects//instances//databases/`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\admin\database\v1\Database
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function getDatabase($name, $optionalArgs = [])
+ {
+ $request = new GetDatabaseRequest();
+ $request->setName($name);
+
+ $mergedSettings = $this->defaultCallSettings['getDatabase']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'GetDatabase',
+ $mergedSettings,
+ $this->descriptors['getDatabase']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Updates the schema of a Cloud Spanner database by
+ * creating/altering/dropping tables, columns, indexes, etc. The returned
+ * [long-running operation][google.longrunning.Operation] will have a name of
+ * the format `/operations/` and can be used to
+ * track execution of the schema change(s). The
+ * [metadata][google.longrunning.Operation.metadata] field type is
+ * [UpdateDatabaseDdlMetadata][google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata]. The operation has no response.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+ * $statements = [];
+ * $operationResponse = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements);
+ * $operationResponse->pollUntilComplete();
+ * if ($operationResponse->operationSucceeded()) {
+ * // operation succeeded and returns no value
+ * } else {
+ * $error = $operationResponse->getError();
+ * // handleError($error)
+ * }
+ *
+ * // OR start the operation, keep the operation name, and resume later
+ * $operationResponse = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements);
+ * $operationName = $operationResponse->getName();
+ * // ... do other work
+ * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'updateDatabaseDdl');
+ * while (!$newOperationResponse->isDone()) {
+ * // ... do other work
+ * $newOperationResponse->reload();
+ * }
+ * if ($newOperationResponse->operationSucceeded()) {
+ * // operation succeeded and returns no value
+ * } else {
+ * $error = $newOperationResponse->getError();
+ * // handleError($error)
+ * }
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $database Required. The database to update.
+ * @param string[] $statements DDL statements to be applied to the database.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type string $operationId
+ * If empty, the new update request is assigned an
+ * automatically-generated operation ID. Otherwise, `operation_id`
+ * is used to construct the name of the resulting
+ * [Operation][google.longrunning.Operation].
+ *
+ * Specifying an explicit operation ID simplifies determining
+ * whether the statements were executed in the event that the
+ * [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] call is replayed,
+ * or the return value is otherwise lost: the [database][google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.database] and
+ * `operation_id` fields can be combined to form the
+ * [name][google.longrunning.Operation.name] of the resulting
+ * [longrunning.Operation][google.longrunning.Operation]: `/operations/`.
+ *
+ * `operation_id` should be unique within the database, and must be
+ * a valid identifier: `[a-zA-Z][a-zA-Z0-9_]*`. Note that
+ * automatically-generated operation IDs always begin with an
+ * underscore. If the named operation already exists,
+ * [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] returns
+ * `ALREADY_EXISTS`.
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\longrunning\Operation
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function updateDatabaseDdl($database, $statements, $optionalArgs = [])
+ {
+ $request = new UpdateDatabaseDdlRequest();
+ $request->setDatabase($database);
+ foreach ($statements as $elem) {
+ $request->addStatements($elem);
+ }
+ if (isset($optionalArgs['operationId'])) {
+ $request->setOperationId($optionalArgs['operationId']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['updateDatabaseDdl']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'UpdateDatabaseDdl',
+ $mergedSettings,
+ $this->descriptors['updateDatabaseDdl']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Drops (aka deletes) a Cloud Spanner database.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+ * $databaseAdminClient->dropDatabase($formattedDatabase);
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $database Required. The database to be dropped.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function dropDatabase($database, $optionalArgs = [])
+ {
+ $request = new DropDatabaseRequest();
+ $request->setDatabase($database);
+
+ $mergedSettings = $this->defaultCallSettings['dropDatabase']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'DropDatabase',
+ $mergedSettings,
+ $this->descriptors['dropDatabase']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Returns the schema of a Cloud Spanner database as a list of formatted
+ * DDL statements. This method does not show pending schema updates, those may
+ * be queried using the [Operations][google.longrunning.Operations] API.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+ * $response = $databaseAdminClient->getDatabaseDdl($formattedDatabase);
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $database Required. The database whose schema we wish to get.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\admin\database\v1\GetDatabaseDdlResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function getDatabaseDdl($database, $optionalArgs = [])
+ {
+ $request = new GetDatabaseDdlRequest();
+ $request->setDatabase($database);
+
+ $mergedSettings = $this->defaultCallSettings['getDatabaseDdl']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'GetDatabaseDdl',
+ $mergedSettings,
+ $this->descriptors['getDatabaseDdl']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Sets the access control policy on a database resource. Replaces any
+ * existing policy.
+ *
+ * Authorization requires `spanner.databases.setIamPolicy` permission on
+ * [resource][google.iam.v1.SetIamPolicyRequest.resource].
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+ * $policy = new Policy();
+ * $response = $databaseAdminClient->setIamPolicy($formattedResource, $policy);
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $resource REQUIRED: The resource for which the policy is being specified.
+ * `resource` is usually specified as a path. For example, a Project
+ * resource is specified as `projects/{project}`.
+ * @param Policy $policy REQUIRED: The complete policy to be applied to the `resource`. The size of
+ * the policy is limited to a few 10s of KB. An empty policy is a
+ * valid policy but certain Cloud Platform services (such as Projects)
+ * might reject them.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\iam\v1\Policy
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function setIamPolicy($resource, $policy, $optionalArgs = [])
+ {
+ $request = new SetIamPolicyRequest();
+ $request->setResource($resource);
+ $request->setPolicy($policy);
+
+ $mergedSettings = $this->defaultCallSettings['setIamPolicy']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'SetIamPolicy',
+ $mergedSettings,
+ $this->descriptors['setIamPolicy']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Gets the access control policy for a database resource. Returns an empty
+ * policy if a database exists but does not have a policy set.
+ *
+ * Authorization requires `spanner.databases.getIamPolicy` permission on
+ * [resource][google.iam.v1.GetIamPolicyRequest.resource].
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+ * $response = $databaseAdminClient->getIamPolicy($formattedResource);
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $resource REQUIRED: The resource for which the policy is being requested.
+ * `resource` is usually specified as a path. For example, a Project
+ * resource is specified as `projects/{project}`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\iam\v1\Policy
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function getIamPolicy($resource, $optionalArgs = [])
+ {
+ $request = new GetIamPolicyRequest();
+ $request->setResource($resource);
+
+ $mergedSettings = $this->defaultCallSettings['getIamPolicy']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'GetIamPolicy',
+ $mergedSettings,
+ $this->descriptors['getIamPolicy']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Returns permissions that the caller has on the specified database resource.
+ *
+ * Attempting this RPC on a non-existent Cloud Spanner database will result in
+ * a NOT_FOUND error if the user has `spanner.databases.list` permission on
+ * the containing Cloud Spanner instance. Otherwise returns an empty set of
+ * permissions.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $databaseAdminClient = new DatabaseAdminClient();
+ * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+ * $permissions = [];
+ * $response = $databaseAdminClient->testIamPermissions($formattedResource, $permissions);
+ * } finally {
+ * $databaseAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $resource REQUIRED: The resource for which the policy detail is being requested.
+ * `resource` is usually specified as a path. For example, a Project
+ * resource is specified as `projects/{project}`.
+ * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with
+ * wildcards (such as '*' or 'storage.*') are not allowed. For more
+ * information see
+ * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions).
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\iam\v1\TestIamPermissionsResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function testIamPermissions($resource, $permissions, $optionalArgs = [])
+ {
+ $request = new TestIamPermissionsRequest();
+ $request->setResource($resource);
+ foreach ($permissions as $elem) {
+ $request->addPermissions($elem);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['testIamPermissions']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->databaseAdminStub,
+ 'TestIamPermissions',
+ $mergedSettings,
+ $this->descriptors['testIamPermissions']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Initiates an orderly shutdown in which preexisting calls continue but new
+ * calls are immediately cancelled.
+ */
+ public function close()
+ {
+ $this->databaseAdminStub->close();
+ }
+
+ private function createCredentialsCallback()
+ {
+ return $this->grpcCredentialsHelper->createCallCredentialsCallback();
+ }
+}
diff --git a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json
new file mode 100644
index 000000000000..efa919a0a7d8
--- /dev/null
+++ b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json
@@ -0,0 +1,73 @@
+{
+ "interfaces": {
+ "google.spanner.admin.database.v1.DatabaseAdmin": {
+ "retry_codes": {
+ "retry_codes_def": {
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": []
+ }
+ },
+ "retry_params": {
+ "default": {
+ "initial_retry_delay_millis": 1000,
+ "retry_delay_multiplier": 1.3,
+ "max_retry_delay_millis": 32000,
+ "initial_rpc_timeout_millis": 60000,
+ "rpc_timeout_multiplier": 1.0,
+ "max_rpc_timeout_millis": 60000,
+ "total_timeout_millis": 600000
+ }
+ },
+ "methods": {
+ "ListDatabases": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "CreateDatabase": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "GetDatabase": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "UpdateDatabaseDdl": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "DropDatabase": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "GetDatabaseDdl": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "SetIamPolicy": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "GetIamPolicy": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "TestIamPermissions": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php
new file mode 100644
index 000000000000..c64fbb7840ae
--- /dev/null
+++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php
@@ -0,0 +1,1243 @@
+listInstanceConfigs($formattedParent);
+ * foreach ($pagedResponse->iterateAllElements() as $element) {
+ * // doSomethingWith($element);
+ * }
+ *
+ * // OR iterate over pages of elements, with the maximum page size set to 5
+ * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent, ['pageSize' => 5]);
+ * foreach ($pagedResponse->iteratePages() as $page) {
+ * foreach ($page as $element) {
+ * // doSomethingWith($element);
+ * }
+ * }
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * Many parameters require resource names to be formatted in a particular way. To assist
+ * with these names, this class includes a format method for each type of name, and additionally
+ * a parse method to extract the individual identifiers contained within names that are
+ * returned.
+ */
+class InstanceAdminClient
+{
+ /**
+ * The default address of the service.
+ */
+ const SERVICE_ADDRESS = 'spanner.googleapis.com';
+
+ /**
+ * The default port of the service.
+ */
+ const DEFAULT_SERVICE_PORT = 443;
+
+ /**
+ * The default timeout for non-retrying methods.
+ */
+ const DEFAULT_TIMEOUT_MILLIS = 30000;
+
+ /**
+ * The name of the code generator, to be included in the agent header.
+ */
+ const CODEGEN_NAME = 'gapic';
+
+ /**
+ * The code generator version, to be included in the agent header.
+ */
+ const CODEGEN_VERSION = '0.1.0';
+
+ private static $projectNameTemplate;
+ private static $instanceConfigNameTemplate;
+ private static $instanceNameTemplate;
+
+ private $grpcCredentialsHelper;
+ private $instanceAdminStub;
+ private $scopes;
+ private $defaultCallSettings;
+ private $descriptors;
+ private $operationsClient;
+
+ /**
+ * Formats a string containing the fully-qualified path to represent
+ * a project resource.
+ */
+ public static function formatProjectName($project)
+ {
+ return self::getProjectNameTemplate()->render([
+ 'project' => $project,
+ ]);
+ }
+
+ /**
+ * Formats a string containing the fully-qualified path to represent
+ * a instance_config resource.
+ */
+ public static function formatInstanceConfigName($project, $instanceConfig)
+ {
+ return self::getInstanceConfigNameTemplate()->render([
+ 'project' => $project,
+ 'instance_config' => $instanceConfig,
+ ]);
+ }
+
+ /**
+ * Formats a string containing the fully-qualified path to represent
+ * a instance resource.
+ */
+ public static function formatInstanceName($project, $instance)
+ {
+ return self::getInstanceNameTemplate()->render([
+ 'project' => $project,
+ 'instance' => $instance,
+ ]);
+ }
+
+ /**
+ * Parses the project from the given fully-qualified path which
+ * represents a project resource.
+ */
+ public static function parseProjectFromProjectName($projectName)
+ {
+ return self::getProjectNameTemplate()->match($projectName)['project'];
+ }
+
+ /**
+ * Parses the project from the given fully-qualified path which
+ * represents a instance_config resource.
+ */
+ public static function parseProjectFromInstanceConfigName($instanceConfigName)
+ {
+ return self::getInstanceConfigNameTemplate()->match($instanceConfigName)['project'];
+ }
+
+ /**
+ * Parses the instance_config from the given fully-qualified path which
+ * represents a instance_config resource.
+ */
+ public static function parseInstanceConfigFromInstanceConfigName($instanceConfigName)
+ {
+ return self::getInstanceConfigNameTemplate()->match($instanceConfigName)['instance_config'];
+ }
+
+ /**
+ * Parses the project from the given fully-qualified path which
+ * represents a instance resource.
+ */
+ public static function parseProjectFromInstanceName($instanceName)
+ {
+ return self::getInstanceNameTemplate()->match($instanceName)['project'];
+ }
+
+ /**
+ * Parses the instance from the given fully-qualified path which
+ * represents a instance resource.
+ */
+ public static function parseInstanceFromInstanceName($instanceName)
+ {
+ return self::getInstanceNameTemplate()->match($instanceName)['instance'];
+ }
+
+ private static function getProjectNameTemplate()
+ {
+ if (self::$projectNameTemplate == null) {
+ self::$projectNameTemplate = new PathTemplate('projects/{project}');
+ }
+
+ return self::$projectNameTemplate;
+ }
+
+ private static function getInstanceConfigNameTemplate()
+ {
+ if (self::$instanceConfigNameTemplate == null) {
+ self::$instanceConfigNameTemplate = new PathTemplate('projects/{project}/instanceConfigs/{instance_config}');
+ }
+
+ return self::$instanceConfigNameTemplate;
+ }
+
+ private static function getInstanceNameTemplate()
+ {
+ if (self::$instanceNameTemplate == null) {
+ self::$instanceNameTemplate = new PathTemplate('projects/{project}/instances/{instance}');
+ }
+
+ return self::$instanceNameTemplate;
+ }
+
+ private static function getPageStreamingDescriptors()
+ {
+ $listInstanceConfigsPageStreamingDescriptor =
+ new PageStreamingDescriptor([
+ 'requestPageTokenField' => 'page_token',
+ 'requestPageSizeField' => 'page_size',
+ 'responsePageTokenField' => 'next_page_token',
+ 'resourceField' => 'instance_configs',
+ ]);
+ $listInstancesPageStreamingDescriptor =
+ new PageStreamingDescriptor([
+ 'requestPageTokenField' => 'page_token',
+ 'requestPageSizeField' => 'page_size',
+ 'responsePageTokenField' => 'next_page_token',
+ 'resourceField' => 'instances',
+ ]);
+
+ $pageStreamingDescriptors = [
+ 'listInstanceConfigs' => $listInstanceConfigsPageStreamingDescriptor,
+ 'listInstances' => $listInstancesPageStreamingDescriptor,
+ ];
+
+ return $pageStreamingDescriptors;
+ }
+
+ public static function getLongRunningDescriptors()
+ {
+ return [
+ 'createInstance' => [
+ 'operationReturnType' => '\google\spanner\admin\instance\v1\Instance',
+ 'metadataReturnType' => '\google\spanner\admin\instance\v1\CreateInstanceMetadata',
+ ],
+ 'updateInstance' => [
+ 'operationReturnType' => '\google\spanner\admin\instance\v1\Instance',
+ 'metadataReturnType' => '\google\spanner\admin\instance\v1\UpdateInstanceMetadata',
+ ],
+ ];
+ }
+
+ /**
+ * Return an OperationsClient object with the same endpoint as $this.
+ *
+ * @return \Google\GAX\LongRunning\OperationsClient
+ */
+ public function getOperationsClient()
+ {
+ return $this->operationsClient;
+ }
+
+ /**
+ * Resume an existing long running operation that was previously started
+ * by a long running API method. If $methodName is not provided, or does
+ * not match a long running API method, then the operation can still be
+ * resumed, but the OperationResponse object will not deserialize the
+ * final response.
+ *
+ * @param string $operationName The name of the long running operation
+ * @param string $methodName The name of the method used to start the operation
+ *
+ * @return \Google\GAX\OperationResponse
+ */
+ public function resumeOperation($operationName, $methodName = null)
+ {
+ $lroDescriptors = self::getLongRunningDescriptors();
+ if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) {
+ $options = $lroDescriptors[$methodName];
+ } else {
+ $options = [];
+ }
+ $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options);
+ $operation->reload();
+
+ return $operation;
+ }
+
+ // TODO(garrettjones): add channel (when supported in gRPC)
+ /**
+ * Constructor.
+ *
+ * @param array $options {
+ * Optional. Options for configuring the service API wrapper.
+ *
+ * @type string $serviceAddress The domain name of the API remote host.
+ * Default 'spanner.googleapis.com'.
+ * @type mixed $port The port on which to connect to the remote host. Default 443.
+ * @type \Grpc\ChannelCredentials $sslCreds
+ * A `ChannelCredentials` for use with an SSL-enabled channel.
+ * Default: a credentials object returned from
+ * \Grpc\ChannelCredentials::createSsl()
+ * @type array $scopes A string array of scopes to use when acquiring credentials.
+ * Default the scopes for the Google Cloud Spanner Instance Admin API.
+ * @type array $retryingOverride
+ * An associative array of string => RetryOptions, where the keys
+ * are method names (e.g. 'createFoo'), that overrides default retrying
+ * settings. A value of null indicates that the method in question should
+ * not retry.
+ * @type int $timeoutMillis The timeout in milliseconds to use for calls
+ * that don't use retries. For calls that use retries,
+ * set the timeout in RetryOptions.
+ * Default: 30000 (30 seconds)
+ * @type string $appName The codename of the calling service. Default 'gax'.
+ * @type string $appVersion The version of the calling service.
+ * Default: the current version of GAX.
+ * @type \Google\Auth\CredentialsLoader $credentialsLoader
+ * A CredentialsLoader object created using the
+ * Google\Auth library.
+ * }
+ */
+ public function __construct($options = [])
+ {
+ $defaultOptions = [
+ 'serviceAddress' => self::SERVICE_ADDRESS,
+ 'port' => self::DEFAULT_SERVICE_PORT,
+ 'scopes' => [
+ 'https://www.googleapis.com/auth/cloud-platform',
+ 'https://www.googleapis.com/auth/spanner.admin',
+ ],
+ 'retryingOverride' => null,
+ 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
+ 'appName' => 'gax',
+ 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ ];
+ $options = array_merge($defaultOptions, $options);
+
+ if (array_key_exists('operationsClient', $options)) {
+ $this->operationsClient = $options['operationsClient'];
+ } else {
+ $this->operationsClient = new OperationsClient([
+ 'serviceAddress' => $options['serviceAddress'],
+ 'scopes' => $options['scopes'],
+ ]);
+ }
+
+ $headerDescriptor = new AgentHeaderDescriptor([
+ 'clientName' => $options['appName'],
+ 'clientVersion' => $options['appVersion'],
+ 'codeGenName' => self::CODEGEN_NAME,
+ 'codeGenVersion' => self::CODEGEN_VERSION,
+ 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'phpVersion' => phpversion(),
+ ]);
+
+ $defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
+ $this->descriptors = [
+ 'listInstanceConfigs' => $defaultDescriptors,
+ 'getInstanceConfig' => $defaultDescriptors,
+ 'listInstances' => $defaultDescriptors,
+ 'getInstance' => $defaultDescriptors,
+ 'createInstance' => $defaultDescriptors,
+ 'updateInstance' => $defaultDescriptors,
+ 'deleteInstance' => $defaultDescriptors,
+ 'setIamPolicy' => $defaultDescriptors,
+ 'getIamPolicy' => $defaultDescriptors,
+ 'testIamPermissions' => $defaultDescriptors,
+ ];
+ $pageStreamingDescriptors = self::getPageStreamingDescriptors();
+ foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) {
+ $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor;
+ }
+ $longRunningDescriptors = self::getLongRunningDescriptors();
+ foreach ($longRunningDescriptors as $method => $longRunningDescriptor) {
+ $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient];
+ }
+
+ $clientConfigJsonString = file_get_contents(__DIR__.'/resources/instance_admin_client_config.json');
+ $clientConfig = json_decode($clientConfigJsonString, true);
+ $this->defaultCallSettings =
+ CallSettings::load(
+ 'google.spanner.admin.instance.v1.InstanceAdmin',
+ $clientConfig,
+ $options['retryingOverride'],
+ GrpcConstants::getStatusCodeNames(),
+ $options['timeoutMillis']
+ );
+
+ $this->scopes = $options['scopes'];
+
+ $createStubOptions = [];
+ if (array_key_exists('sslCreds', $options)) {
+ $createStubOptions['sslCreds'] = $options['sslCreds'];
+ }
+ $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions);
+ $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions);
+
+ $createInstanceAdminStubFunction = function ($hostname, $opts) {
+ return new InstanceAdminGrpcClient($hostname, $opts);
+ };
+ if (array_key_exists('createInstanceAdminStubFunction', $options)) {
+ $createInstanceAdminStubFunction = $options['createInstanceAdminStubFunction'];
+ }
+ $this->instanceAdminStub = $this->grpcCredentialsHelper->createStub(
+ $createInstanceAdminStubFunction,
+ $options['serviceAddress'],
+ $options['port'],
+ $createStubOptions
+ );
+ }
+
+ /**
+ * Lists the supported instance configurations for a given project.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]");
+ * // Iterate through all elements
+ * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent);
+ * foreach ($pagedResponse->iterateAllElements() as $element) {
+ * // doSomethingWith($element);
+ * }
+ *
+ * // OR iterate over pages of elements, with the maximum page size set to 5
+ * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent, ['pageSize' => 5]);
+ * foreach ($pagedResponse->iteratePages() as $page) {
+ * foreach ($page as $element) {
+ * // doSomethingWith($element);
+ * }
+ * }
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $parent Required. The name of the project for which a list of supported instance
+ * configurations is requested. Values are of the form
+ * `projects/`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type int $pageSize
+ * The maximum number of resources contained in the underlying API
+ * response. The API may return fewer values in a page, even if
+ * there are additional values to be retrieved.
+ * @type string $pageToken
+ * A page token is used to specify a page of values to be returned.
+ * If no page token is specified (the default), the first page
+ * of values will be returned. Any page token used here must have
+ * been generated by a previous call to the API.
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \Google\GAX\PagedListResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function listInstanceConfigs($parent, $optionalArgs = [])
+ {
+ $request = new ListInstanceConfigsRequest();
+ $request->setParent($parent);
+ if (isset($optionalArgs['pageSize'])) {
+ $request->setPageSize($optionalArgs['pageSize']);
+ }
+ if (isset($optionalArgs['pageToken'])) {
+ $request->setPageToken($optionalArgs['pageToken']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['listInstanceConfigs']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'ListInstanceConfigs',
+ $mergedSettings,
+ $this->descriptors['listInstanceConfigs']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Gets information about a particular instance configuration.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedName = InstanceAdminClient::formatInstanceConfigName("[PROJECT]", "[INSTANCE_CONFIG]");
+ * $response = $instanceAdminClient->getInstanceConfig($formattedName);
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $name Required. The name of the requested instance configuration. Values are of
+ * the form `projects//instanceConfigs/`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\admin\instance\v1\InstanceConfig
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function getInstanceConfig($name, $optionalArgs = [])
+ {
+ $request = new GetInstanceConfigRequest();
+ $request->setName($name);
+
+ $mergedSettings = $this->defaultCallSettings['getInstanceConfig']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'GetInstanceConfig',
+ $mergedSettings,
+ $this->descriptors['getInstanceConfig']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Lists all instances in the given project.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]");
+ * // Iterate through all elements
+ * $pagedResponse = $instanceAdminClient->listInstances($formattedParent);
+ * foreach ($pagedResponse->iterateAllElements() as $element) {
+ * // doSomethingWith($element);
+ * }
+ *
+ * // OR iterate over pages of elements, with the maximum page size set to 5
+ * $pagedResponse = $instanceAdminClient->listInstances($formattedParent, ['pageSize' => 5]);
+ * foreach ($pagedResponse->iteratePages() as $page) {
+ * foreach ($page as $element) {
+ * // doSomethingWith($element);
+ * }
+ * }
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $parent Required. The name of the project for which a list of instances is
+ * requested. Values are of the form `projects/`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type int $pageSize
+ * The maximum number of resources contained in the underlying API
+ * response. The API may return fewer values in a page, even if
+ * there are additional values to be retrieved.
+ * @type string $pageToken
+ * A page token is used to specify a page of values to be returned.
+ * If no page token is specified (the default), the first page
+ * of values will be returned. Any page token used here must have
+ * been generated by a previous call to the API.
+ * @type string $filter
+ * An expression for filtering the results of the request. Filter rules are
+ * case insensitive. The fields eligible for filtering are:
+ *
+ * * name
+ * * display_name
+ * * labels.key where key is the name of a label
+ *
+ * Some examples of using filters are:
+ *
+ * * name:* --> The instance has a name.
+ * * name:Howl --> The instance's name contains the string "howl".
+ * * name:HOWL --> Equivalent to above.
+ * * NAME:howl --> Equivalent to above.
+ * * labels.env:* --> The instance has the label "env".
+ * * labels.env:dev --> The instance has the label "env" and the value of
+ * the label contains the string "dev".
+ * * name:howl labels.env:dev --> The instance's name contains "howl" and
+ * it has the label "env" with its value
+ * containing "dev".
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \Google\GAX\PagedListResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function listInstances($parent, $optionalArgs = [])
+ {
+ $request = new ListInstancesRequest();
+ $request->setParent($parent);
+ if (isset($optionalArgs['pageSize'])) {
+ $request->setPageSize($optionalArgs['pageSize']);
+ }
+ if (isset($optionalArgs['pageToken'])) {
+ $request->setPageToken($optionalArgs['pageToken']);
+ }
+ if (isset($optionalArgs['filter'])) {
+ $request->setFilter($optionalArgs['filter']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['listInstances']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'ListInstances',
+ $mergedSettings,
+ $this->descriptors['listInstances']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Gets information about a particular instance.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]");
+ * $response = $instanceAdminClient->getInstance($formattedName);
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $name Required. The name of the requested instance. Values are of the form
+ * `projects//instances/`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\admin\instance\v1\Instance
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function getInstance($name, $optionalArgs = [])
+ {
+ $request = new GetInstanceRequest();
+ $request->setName($name);
+
+ $mergedSettings = $this->defaultCallSettings['getInstance']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'GetInstance',
+ $mergedSettings,
+ $this->descriptors['getInstance']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Creates an instance and begins preparing it to begin serving. The
+ * returned [long-running operation][google.longrunning.Operation]
+ * can be used to track the progress of preparing the new
+ * instance. The instance name is assigned by the caller. If the
+ * named instance already exists, `CreateInstance` returns
+ * `ALREADY_EXISTS`.
+ *
+ * Immediately upon completion of this request:
+ *
+ * * The instance is readable via the API, with all requested attributes
+ * but no allocated resources. Its state is `CREATING`.
+ *
+ * Until completion of the returned operation:
+ *
+ * * Cancelling the operation renders the instance immediately unreadable
+ * via the API.
+ * * The instance can be deleted.
+ * * All other attempts to modify the instance are rejected.
+ *
+ * Upon completion of the returned operation:
+ *
+ * * Billing for all successfully-allocated resources begins (some types
+ * may have lower than the requested levels).
+ * * Databases can be created in the instance.
+ * * The instance's allocated resource levels are readable via the API.
+ * * The instance's state becomes `READY`.
+ *
+ * The returned [long-running operation][google.longrunning.Operation] will
+ * have a name of the format `/operations/` and
+ * can be used to track creation of the instance. The
+ * [metadata][google.longrunning.Operation.metadata] field type is
+ * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata].
+ * The [response][google.longrunning.Operation.response] field type is
+ * [Instance][google.spanner.admin.instance.v1.Instance], if successful.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]");
+ * $instanceId = "";
+ * $instance = new Instance();
+ * $operationResponse = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance);
+ * $operationResponse->pollUntilComplete();
+ * if ($operationResponse->operationSucceeded()) {
+ * $result = $operationResponse->getResult();
+ * // doSomethingWith($result)
+ * } else {
+ * $error = $operationResponse->getError();
+ * // handleError($error)
+ * }
+ *
+ * // OR start the operation, keep the operation name, and resume later
+ * $operationResponse = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance);
+ * $operationName = $operationResponse->getName();
+ * // ... do other work
+ * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'createInstance');
+ * while (!$newOperationResponse->isDone()) {
+ * // ... do other work
+ * $newOperationResponse->reload();
+ * }
+ * if ($newOperationResponse->operationSucceeded()) {
+ * $result = $newOperationResponse->getResult();
+ * // doSomethingWith($result)
+ * } else {
+ * $error = $newOperationResponse->getError();
+ * // handleError($error)
+ * }
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $parent Required. The name of the project in which to create the instance. Values
+ * are of the form `projects/`.
+ * @param string $instanceId Required. The ID of the instance to create. Valid identifiers are of the
+ * form `[a-z][-a-z0-9]*[a-z0-9]` and must be between 6 and 30 characters in
+ * length.
+ * @param Instance $instance Required. The instance to create. The name may be omitted, but if
+ * specified must be `/instances/`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\longrunning\Operation
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function createInstance($parent, $instanceId, $instance, $optionalArgs = [])
+ {
+ $request = new CreateInstanceRequest();
+ $request->setParent($parent);
+ $request->setInstanceId($instanceId);
+ $request->setInstance($instance);
+
+ $mergedSettings = $this->defaultCallSettings['createInstance']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'CreateInstance',
+ $mergedSettings,
+ $this->descriptors['createInstance']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Updates an instance, and begins allocating or releasing resources
+ * as requested. The returned [long-running
+ * operation][google.longrunning.Operation] can be used to track the
+ * progress of updating the instance. If the named instance does not
+ * exist, returns `NOT_FOUND`.
+ *
+ * Immediately upon completion of this request:
+ *
+ * * For resource types for which a decrease in the instance's allocation
+ * has been requested, billing is based on the newly-requested level.
+ *
+ * Until completion of the returned operation:
+ *
+ * * Cancelling the operation sets its metadata's
+ * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins
+ * restoring resources to their pre-request values. The operation
+ * is guaranteed to succeed at undoing all resource changes,
+ * after which point it terminates with a `CANCELLED` status.
+ * * All other attempts to modify the instance are rejected.
+ * * Reading the instance via the API continues to give the pre-request
+ * resource levels.
+ *
+ * Upon completion of the returned operation:
+ *
+ * * Billing begins for all successfully-allocated resources (some types
+ * may have lower than the requested levels).
+ * * All newly-reserved resources are available for serving the instance's
+ * tables.
+ * * The instance's new resource levels are readable via the API.
+ *
+ * The returned [long-running operation][google.longrunning.Operation] will
+ * have a name of the format `/operations/` and
+ * can be used to track the instance modification. The
+ * [metadata][google.longrunning.Operation.metadata] field type is
+ * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata].
+ * The [response][google.longrunning.Operation.response] field type is
+ * [Instance][google.spanner.admin.instance.v1.Instance], if successful.
+ *
+ * Authorization requires `spanner.instances.update` permission on
+ * resource [name][google.spanner.admin.instance.v1.Instance.name].
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $instance = new Instance();
+ * $fieldMask = new FieldMask();
+ * $operationResponse = $instanceAdminClient->updateInstance($instance, $fieldMask);
+ * $operationResponse->pollUntilComplete();
+ * if ($operationResponse->operationSucceeded()) {
+ * $result = $operationResponse->getResult();
+ * // doSomethingWith($result)
+ * } else {
+ * $error = $operationResponse->getError();
+ * // handleError($error)
+ * }
+ *
+ * // OR start the operation, keep the operation name, and resume later
+ * $operationResponse = $instanceAdminClient->updateInstance($instance, $fieldMask);
+ * $operationName = $operationResponse->getName();
+ * // ... do other work
+ * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'updateInstance');
+ * while (!$newOperationResponse->isDone()) {
+ * // ... do other work
+ * $newOperationResponse->reload();
+ * }
+ * if ($newOperationResponse->operationSucceeded()) {
+ * $result = $newOperationResponse->getResult();
+ * // doSomethingWith($result)
+ * } else {
+ * $error = $newOperationResponse->getError();
+ * // handleError($error)
+ * }
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param Instance $instance Required. The instance to update, which must always include the instance
+ * name. Otherwise, only fields mentioned in [][google.spanner.admin.instance.v1.UpdateInstanceRequest.field_mask] need be included.
+ * @param FieldMask $fieldMask Required. A mask specifying which fields in [][google.spanner.admin.instance.v1.UpdateInstanceRequest.instance] should be updated.
+ * The field mask must always be specified; this prevents any future fields in
+ * [][google.spanner.admin.instance.v1.Instance] from being erased accidentally by clients that do not know
+ * about them.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\longrunning\Operation
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function updateInstance($instance, $fieldMask, $optionalArgs = [])
+ {
+ $request = new UpdateInstanceRequest();
+ $request->setInstance($instance);
+ $request->setFieldMask($fieldMask);
+
+ $mergedSettings = $this->defaultCallSettings['updateInstance']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'UpdateInstance',
+ $mergedSettings,
+ $this->descriptors['updateInstance']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Deletes an instance.
+ *
+ * Immediately upon completion of the request:
+ *
+ * * Billing ceases for all of the instance's reserved resources.
+ *
+ * Soon afterward:
+ *
+ * * The instance and *all of its databases* immediately and
+ * irrevocably disappear from the API. All data in the databases
+ * is permanently deleted.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]");
+ * $instanceAdminClient->deleteInstance($formattedName);
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $name Required. The name of the instance to be deleted. Values are of the form
+ * `projects//instances/`
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function deleteInstance($name, $optionalArgs = [])
+ {
+ $request = new DeleteInstanceRequest();
+ $request->setName($name);
+
+ $mergedSettings = $this->defaultCallSettings['deleteInstance']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'DeleteInstance',
+ $mergedSettings,
+ $this->descriptors['deleteInstance']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Sets the access control policy on an instance resource. Replaces any
+ * existing policy.
+ *
+ * Authorization requires `spanner.instances.setIamPolicy` on
+ * [resource][google.iam.v1.SetIamPolicyRequest.resource].
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]");
+ * $policy = new Policy();
+ * $response = $instanceAdminClient->setIamPolicy($formattedResource, $policy);
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $resource REQUIRED: The resource for which the policy is being specified.
+ * `resource` is usually specified as a path. For example, a Project
+ * resource is specified as `projects/{project}`.
+ * @param Policy $policy REQUIRED: The complete policy to be applied to the `resource`. The size of
+ * the policy is limited to a few 10s of KB. An empty policy is a
+ * valid policy but certain Cloud Platform services (such as Projects)
+ * might reject them.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\iam\v1\Policy
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function setIamPolicy($resource, $policy, $optionalArgs = [])
+ {
+ $request = new SetIamPolicyRequest();
+ $request->setResource($resource);
+ $request->setPolicy($policy);
+
+ $mergedSettings = $this->defaultCallSettings['setIamPolicy']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'SetIamPolicy',
+ $mergedSettings,
+ $this->descriptors['setIamPolicy']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Gets the access control policy for an instance resource. Returns an empty
+ * policy if an instance exists but does not have a policy set.
+ *
+ * Authorization requires `spanner.instances.getIamPolicy` on
+ * [resource][google.iam.v1.GetIamPolicyRequest.resource].
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]");
+ * $response = $instanceAdminClient->getIamPolicy($formattedResource);
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $resource REQUIRED: The resource for which the policy is being requested.
+ * `resource` is usually specified as a path. For example, a Project
+ * resource is specified as `projects/{project}`.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\iam\v1\Policy
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function getIamPolicy($resource, $optionalArgs = [])
+ {
+ $request = new GetIamPolicyRequest();
+ $request->setResource($resource);
+
+ $mergedSettings = $this->defaultCallSettings['getIamPolicy']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'GetIamPolicy',
+ $mergedSettings,
+ $this->descriptors['getIamPolicy']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Returns permissions that the caller has on the specified instance resource.
+ *
+ * Attempting this RPC on a non-existent Cloud Spanner instance resource will
+ * result in a NOT_FOUND error if the user has `spanner.instances.list`
+ * permission on the containing Google Cloud Project. Otherwise returns an
+ * empty set of permissions.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $instanceAdminClient = new InstanceAdminClient();
+ * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]");
+ * $permissions = [];
+ * $response = $instanceAdminClient->testIamPermissions($formattedResource, $permissions);
+ * } finally {
+ * $instanceAdminClient->close();
+ * }
+ * ```
+ *
+ * @param string $resource REQUIRED: The resource for which the policy detail is being requested.
+ * `resource` is usually specified as a path. For example, a Project
+ * resource is specified as `projects/{project}`.
+ * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with
+ * wildcards (such as '*' or 'storage.*') are not allowed. For more
+ * information see
+ * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions).
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\iam\v1\TestIamPermissionsResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function testIamPermissions($resource, $permissions, $optionalArgs = [])
+ {
+ $request = new TestIamPermissionsRequest();
+ $request->setResource($resource);
+ foreach ($permissions as $elem) {
+ $request->addPermissions($elem);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['testIamPermissions']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->instanceAdminStub,
+ 'TestIamPermissions',
+ $mergedSettings,
+ $this->descriptors['testIamPermissions']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Initiates an orderly shutdown in which preexisting calls continue but new
+ * calls are immediately cancelled.
+ */
+ public function close()
+ {
+ $this->instanceAdminStub->close();
+ }
+
+ private function createCredentialsCallback()
+ {
+ return $this->grpcCredentialsHelper->createCallCredentialsCallback();
+ }
+}
diff --git a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json
new file mode 100644
index 000000000000..23dbca4fe655
--- /dev/null
+++ b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json
@@ -0,0 +1,78 @@
+{
+ "interfaces": {
+ "google.spanner.admin.instance.v1.InstanceAdmin": {
+ "retry_codes": {
+ "retry_codes_def": {
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": []
+ }
+ },
+ "retry_params": {
+ "default": {
+ "initial_retry_delay_millis": 1000,
+ "retry_delay_multiplier": 1.3,
+ "max_retry_delay_millis": 32000,
+ "initial_rpc_timeout_millis": 60000,
+ "rpc_timeout_multiplier": 1.0,
+ "max_rpc_timeout_millis": 60000,
+ "total_timeout_millis": 600000
+ }
+ },
+ "methods": {
+ "ListInstanceConfigs": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "GetInstanceConfig": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "ListInstances": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "GetInstance": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "CreateInstance": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "UpdateInstance": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "DeleteInstance": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "SetIamPolicy": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "GetIamPolicy": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "TestIamPermissions": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Spanner/Bytes.php b/src/Spanner/Bytes.php
new file mode 100644
index 000000000000..f0e3e4d6daf3
--- /dev/null
+++ b/src/Spanner/Bytes.php
@@ -0,0 +1,112 @@
+spanner();
+ *
+ * $bytes = $spanner->bytes('hello world');
+ * ```
+ *
+ * ```
+ * // Bytes objects can be cast to strings for easy display.
+ * echo (string) $bytes;
+ * ```
+ */
+class Bytes implements ValueInterface
+{
+ /**
+ * @var string|resource|StreamInterface
+ */
+ private $value;
+
+ /**
+ * @param string|resource|StreamInterface $value The bytes value.
+ */
+ public function __construct($value)
+ {
+ $this->value = Psr7\stream_for($value);
+ }
+
+ /**
+ * Get the bytes as a stream.
+ *
+ * Example:
+ * ```
+ * $stream = $bytes->get();
+ * ```
+ *
+ * @return StreamInterface
+ */
+ public function get()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Get the type.
+ *
+ * Example:
+ * ```
+ * echo $bytes->type();
+ * ```
+ *
+ * @return string
+ */
+ public function type()
+ {
+ return ValueMapper::TYPE_BYTES;
+ }
+
+ /**
+ * Format the value as a string.
+ *
+ * Example:
+ * ```
+ * echo $bytes->formatAsString();
+ * ```
+ *
+ * @return string
+ */
+ public function formatAsString()
+ {
+ return base64_encode((string) $this->value);
+ }
+
+ /**
+ * Format the value as a string.
+ *
+ * @return string
+ * @access private
+ */
+ public function __toString()
+ {
+ return $this->formatAsString();
+ }
+}
diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php
new file mode 100644
index 000000000000..d8eea3e9aead
--- /dev/null
+++ b/src/Spanner/Configuration.php
@@ -0,0 +1,194 @@
+spanner();
+ *
+ * $configuration = $spanner->configuration('regional-europe-west');
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig InstanceConfig
+ * @codingStandardsIgnoreEnd
+ */
+class Configuration
+{
+ /**
+ * @var ConnectionInterface
+ */
+ private $connection;
+
+ /**
+ * @var string
+ */
+ private $projectId;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var array
+ */
+ private $info;
+
+ /**
+ * Create a configuration instance.
+ *
+ * @param ConnectionInterface $connection A service connection for the
+ * Spanner API.
+ * @param string $projectId The current project ID.
+ * @param string $name The simple configuration name.
+ * @param array $info [optional] A service representation of the
+ * configuration.
+ */
+ public function __construct(
+ ConnectionInterface $connection,
+ $projectId,
+ $name,
+ array $info = []
+ ) {
+ $this->connection = $connection;
+ $this->projectId = $projectId;
+ $this->name = $name;
+ $this->info = $info;
+ }
+
+ /**
+ * Return the configuration name.
+ *
+ * Example:
+ * ```
+ * $name = $configuration->name();
+ * ```
+ *
+ * @return string
+ */
+ public function name()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Return the service representation of the configuration.
+ *
+ * This method may require a service call.
+ *
+ * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope.
+ *
+ * Example:
+ * ```
+ * $info = $configuration->info();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @param array $options [optional] Configuration options.
+ * @return array [InstanceConfig](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig)
+ * @codingStandardsIgnoreEnd
+ */
+ public function info(array $options = [])
+ {
+ if (!$this->info) {
+ $this->reload($options);
+ }
+
+ return $this->info;
+ }
+
+ /**
+ * Check if the configuration exists.
+ *
+ * This method requires a service call.
+ *
+ * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope.
+ *
+ * Example:
+ * ```
+ * if ($configuration->exists()) {
+ * echo 'Configuration exists!';
+ * }
+ * ```
+ *
+ * @param array $options [optional] Configuration options.
+ * @return bool
+ */
+ public function exists(array $options = [])
+ {
+ try {
+ $this->reload($options = []);
+ } catch (NotFoundException $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Fetch a fresh representation of the configuration from the service.
+ *
+ * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope.
+ *
+ * Example:
+ * ```
+ * $info = $configuration->reload();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @param array $options [optional] Configuration options.
+ * @return array [InstanceConfig](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig)
+ * @codingStandardsIgnoreEnd
+ */
+ public function reload(array $options = [])
+ {
+ $this->info = $this->connection->getConfig($options + [
+ 'name' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $this->name),
+ 'projectId' => $this->projectId
+ ]);
+
+ return $this->info;
+ }
+
+ /**
+ * A more readable representation of the object.
+ *
+ * @codeCoverageIgnore
+ * @access private
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'connection' => get_class($this->connection),
+ 'projectId' => $this->projectId,
+ 'name' => $this->name,
+ 'info' => $this->info,
+ ];
+ }
+}
diff --git a/src/Spanner/Connection/ConnectionInterface.php b/src/Spanner/Connection/ConnectionInterface.php
new file mode 100644
index 000000000000..b30bfc20e612
--- /dev/null
+++ b/src/Spanner/Connection/ConnectionInterface.php
@@ -0,0 +1,171 @@
+ 'setInsert',
+ 'update' => 'setUpdate',
+ 'upsert' => 'setInsertOrUpdate',
+ 'replace' => 'setReplace',
+ 'delete' => 'setDelete'
+ ];
+
+ /**
+ * @var array
+ */
+ private $lroResponseMappers = [
+ [
+ 'method' => 'updateDatabaseDdl',
+ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata',
+ 'message' => protobuf\EmptyC::class
+ ], [
+ 'method' => 'createDatabase',
+ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata',
+ 'message' => Database::class
+ ], [
+ 'method' => 'createInstance',
+ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata',
+ 'message' => Instance::class
+ ], [
+ 'method' => 'updateInstance',
+ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata',
+ 'message' => Instance::class
+ ]
+ ];
+
+ /**
+ * @var array
+ */
+ private $longRunningGrpcClients;
+
+ /**
+ * @param array $config [optional]
+ */
+ public function __construct(array $config = [])
+ {
+ $this->codec = new PhpArray([
+ 'customFilters' => [
+ 'commitTimestamp' => function ($v) {
+ return $this->formatTimestampFromApi($v);
+ },
+ 'readTimestamp' => function ($v) {
+ return $this->formatTimestampFromApi($v);
+ }
+ ]
+ ]);
+
+ $config['codec'] = $this->codec;
+ $this->setRequestWrapper(new GrpcRequestWrapper($config));
+
+ $grpcConfig = $this->getGaxConfig(VeneerSpannerClient::VERSION);
+ $this->instanceAdminClient = new InstanceAdminClient($grpcConfig);
+ $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig);
+ $this->spannerClient = new SpannerClient($grpcConfig);
+ $this->operationsClient = $this->instanceAdminClient->getOperationsClient();
+
+ $this->longRunningGrpcClients = [
+ $this->instanceAdminClient,
+ $this->databaseAdminClient
+ ];
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function listConfigs(array $args = [])
+ {
+ return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [
+ $this->pluck('projectId', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function getConfig(array $args = [])
+ {
+ return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [
+ $this->pluck('name', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function listInstances(array $args = [])
+ {
+ return $this->send([$this->instanceAdminClient, 'listInstances'], [
+ $this->pluck('projectId', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function getInstance(array $args = [])
+ {
+ return $this->send([$this->instanceAdminClient, 'getInstance'], [
+ $this->pluck('name', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function createInstance(array $args = [])
+ {
+ $instance = $this->instanceObject($args, true);
+ $res = $this->send([$this->instanceAdminClient, 'createInstance'], [
+ $this->pluck('projectId', $args),
+ $this->pluck('instanceId', $args),
+ $instance,
+ $args
+ ]);
+
+ return $this->operationToArray($res, $this->codec, $this->lroResponseMappers);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function updateInstance(array $args = [])
+ {
+ $instanceObject = $this->instanceObject($args);
+
+ $mask = array_keys($instanceObject->serialize(new PhpArray(['useCamelCase' => false])));
+
+ $fieldMask = (new protobuf\FieldMask())->deserialize(['paths' => $mask], $this->codec);
+
+ $res = $this->send([$this->instanceAdminClient, 'updateInstance'], [
+ $instanceObject,
+ $fieldMask,
+ $args
+ ]);
+
+ return $this->operationToArray($res, $this->codec, $this->lroResponseMappers);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function deleteInstance(array $args = [])
+ {
+ return $this->send([$this->instanceAdminClient, 'deleteInstance'], [
+ $this->pluck('name', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function getInstanceIamPolicy(array $args = [])
+ {
+ return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [
+ $this->pluck('resource', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function setInstanceIamPolicy(array $args = [])
+ {
+ return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [
+ $this->pluck('resource', $args),
+ $this->pluck('policy', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function testInstanceIamPermissions(array $args = [])
+ {
+ return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [
+ $this->pluck('resource', $args),
+ $this->pluck('permissions', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function listDatabases(array $args = [])
+ {
+ return $this->send([$this->databaseAdminClient, 'listDatabases'], [
+ $this->pluck('instance', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function createDatabase(array $args = [])
+ {
+ $res = $this->send([$this->databaseAdminClient, 'createDatabase'], [
+ $this->pluck('instance', $args),
+ $this->pluck('createStatement', $args),
+ $this->pluck('extraStatements', $args),
+ $args
+ ]);
+
+ return $this->operationToArray($res, $this->codec, $this->lroResponseMappers);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function updateDatabase(array $args = [])
+ {
+ $res = $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [
+ $this->pluck('name', $args),
+ $this->pluck('statements', $args),
+ $args
+ ]);
+
+ return $this->operationToArray($res, $this->codec, $this->lroResponseMappers);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function dropDatabase(array $args = [])
+ {
+ return $this->send([$this->databaseAdminClient, 'dropDatabase'], [
+ $this->pluck('name', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function getDatabaseDDL(array $args = [])
+ {
+ return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [
+ $this->pluck('name', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function getDatabaseIamPolicy(array $args = [])
+ {
+ return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [
+ $this->pluck('resource', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function setDatabaseIamPolicy(array $args = [])
+ {
+ return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [
+ $this->pluck('resource', $args),
+ $this->pluck('policy', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function testDatabaseIamPermissions(array $args = [])
+ {
+ return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [
+ $this->pluck('resource', $args),
+ $this->pluck('permissions', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function createSession(array $args = [])
+ {
+ return $this->send([$this->spannerClient, 'createSession'], [
+ $this->pluck('database', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function getSession(array $args = [])
+ {
+ return $this->send([$this->spannerClient, 'getSession'], [
+ $this->pluck('name', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function deleteSession(array $args = [])
+ {
+ return $this->send([$this->spannerClient, 'deleteSession'], [
+ $this->pluck('name', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function executeSql(array $args = [])
+ {
+ $params = new protobuf\Struct;
+ if (!empty($args['params'])) {
+ $params->deserialize($this->formatStructForApi($args['params']), $this->codec);
+ }
+
+ $args['params'] = $params;
+
+ foreach ($args['paramTypes'] as $key => $param) {
+ $args['paramTypes'][$key] = (new Type)
+ ->deserialize($param, $this->codec);
+ }
+
+ $args['transaction'] = $this->createTransactionSelector($args);
+
+ return $this->send([$this->spannerClient, 'executeSql'], [
+ $this->pluck('session', $args),
+ $this->pluck('sql', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function read(array $args = [])
+ {
+ $keySet = $this->pluck('keySet', $args);
+ $keySet = (new KeySet)
+ ->deserialize($this->formatKeySet($keySet), $this->codec);
+
+ $args['transaction'] = $this->createTransactionSelector($args);
+
+ return $this->send([$this->spannerClient, 'read'], [
+ $this->pluck('session', $args),
+ $this->pluck('table', $args),
+ $this->pluck('columns', $args),
+ $keySet,
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function beginTransaction(array $args = [])
+ {
+ $options = new TransactionOptions;
+
+ if (isset($args['transactionOptions']['readOnly'])) {
+ $ro = $args['transactionOptions']['readOnly'];
+
+ if (isset($ro['minReadTimestamp'])) {
+ $ro['minReadTimestamp'] = $this->formatTimestampForApi($ro['minReadTimestamp']);
+ }
+
+ if (isset($ro['readTimestamp'])) {
+ $ro['readTimestamp'] = $this->formatTimestampForApi($ro['readTimestamp']);
+ }
+
+ $readOnly = (new TransactionOptions\ReadOnly)
+ ->deserialize($ro, $this->codec);
+
+ $options->setReadOnly($readOnly);
+ } else {
+ $readWrite = new TransactionOptions\ReadWrite();
+ $options->setReadWrite($readWrite);
+ }
+
+ return $this->send([$this->spannerClient, 'beginTransaction'], [
+ $this->pluck('session', $args),
+ $options,
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function commit(array $args = [])
+ {
+ $inputMutations = $this->pluck('mutations', $args);
+
+ $mutations = [];
+ if (is_array($inputMutations)) {
+ foreach ($inputMutations as $mutation) {
+ $type = array_keys($mutation)[0];
+ $data = $mutation[$type];
+
+ switch ($type) {
+ case 'insert':
+ case 'update':
+ case 'upsert':
+ case 'replace':
+ $data['values'] = $this->formatListForApi($data['values']);
+
+ $operation = (new Mutation\Write)
+ ->deserialize($data, $this->codec);
+
+ break;
+
+ case 'delete':
+ if (isset($data['keySet'])) {
+ $data['keySet'] = $this->formatKeySet($data['keySet']);
+ }
+
+ $operation = (new Mutation\Delete)
+ ->deserialize($data, $this->codec);
+
+ break;
+ }
+
+ $setterName = $this->mutationSetters[$type];
+ $mutation = new Mutation;
+ $mutation->$setterName($operation);
+ $mutations[] = $mutation;
+ }
+ }
+
+ if (isset($args['singleUseTransaction'])) {
+ $readWrite = (new TransactionOptions\ReadWrite)
+ ->deserialize([], $this->codec);
+
+ $options = new TransactionOptions;
+ $options->setReadWrite($readWrite);
+ $args['singleUseTransaction'] = $options;
+ }
+
+ return $this->send([$this->spannerClient, 'commit'], [
+ $this->pluck('session', $args),
+ $mutations,
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args [optional]
+ */
+ public function rollback(array $args = [])
+ {
+ return $this->send([$this->spannerClient, 'rollback'], [
+ $this->pluck('session', $args),
+ $this->pluck('transactionId', $args),
+ $args
+ ]);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function getOperation(array $args)
+ {
+ $name = $this->pluck('name', $args);
+
+ $operation = $this->getOperationByName($this->databaseAdminClient, $name);
+
+ return $this->operationToArray($operation, $this->codec, $this->lroResponseMappers);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function cancelOperation(array $args)
+ {
+ $name = $this->pluck('name', $args);
+ $method = $this->pluck('method', $args);
+
+ $operation = $this->getOperationByNameAndMethod($name, $method);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function deleteOperation(array $args)
+ {
+ $name = $this->pluck('name', $args);
+ $method = $this->pluck('method', $args);
+
+ $operation = $this->getOperationByNameAndMethod($name, $method);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function listOperations(array $args)
+ {
+ $name = $this->pluck('name', $args);
+ $method = $this->pluck('method', $args);
+ }
+
+ /**
+ * @param array $keySet
+ * @return array Formatted keyset
+ */
+ private function formatKeySet(array $keySet)
+ {
+ if (isset($keySet['keys'])) {
+ $keySet['keys'] = $this->formatListForApi($keySet['keys']);
+ }
+
+ if (isset($keySet['ranges'])) {
+ foreach ($keySet['ranges'] as $index => $rangeItem) {
+ foreach ($rangeItem as $key => $val) {
+ $rangeItem[$key] = $this->formatListForApi($val);
+ }
+
+ $keySet['ranges'][$index] = $rangeItem;
+ }
+ }
+
+ return $keySet;
+ }
+
+ /**
+ * @param array $args
+ * @return array
+ */
+ private function createTransactionSelector(array &$args)
+ {
+ $selector = new TransactionSelector;
+ if (isset($args['transaction'])) {
+ $selector = $selector->deserialize($this->pluck('transaction', $args), $this->codec);
+ } elseif (isset($args['transactionId'])) {
+ $selector = $selector->deserialize(['id' => $this->pluck('transactionId', $args)], $this->codec);
+ }
+
+ return $selector;
+ }
+
+ /**
+ * @param array $args
+ * @param bool $isRequired
+ */
+ private function instanceObject(array &$args, $required = false)
+ {
+ return (new Instance())->deserialize(array_filter([
+ 'name' => $this->pluck('name', $args, $required),
+ 'config' => $this->pluck('config', $args, $required),
+ 'displayName' => $this->pluck('displayName', $args, $required),
+ 'nodeCount' => $this->pluck('nodeCount', $args, $required),
+ 'state' => $this->pluck('state', $args, $required),
+ 'labels' => $this->formatLabelsForApi($this->pluck('labels', $args, $required))
+ ]), $this->codec);
+ }
+}
diff --git a/src/Spanner/Connection/IamDatabase.php b/src/Spanner/Connection/IamDatabase.php
new file mode 100644
index 000000000000..f76eb92b11a6
--- /dev/null
+++ b/src/Spanner/Connection/IamDatabase.php
@@ -0,0 +1,63 @@
+connection = $connection;
+ }
+
+ /**
+ * @param array $args
+ */
+ public function getPolicy(array $args)
+ {
+ return $this->connection->getDatabaseIamPolicy($args);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function setPolicy(array $args)
+ {
+ return $this->connection->setDatabaseIamPolicy($args);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function testPermissions(array $args)
+ {
+ return $this->connection->testDatabaseIamPermissions($args);
+ }
+}
diff --git a/src/Spanner/Connection/IamInstance.php b/src/Spanner/Connection/IamInstance.php
new file mode 100644
index 000000000000..5dac2b7a7c66
--- /dev/null
+++ b/src/Spanner/Connection/IamInstance.php
@@ -0,0 +1,63 @@
+connection = $connection;
+ }
+
+ /**
+ * @param array $args
+ */
+ public function getPolicy(array $args)
+ {
+ return $this->connection->getInstanceIamPolicy($args);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function setPolicy(array $args)
+ {
+ return $this->connection->setInstanceIamPolicy($args);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function testPermissions(array $args)
+ {
+ return $this->connection->testInstanceIamPermissions($args);
+ }
+}
diff --git a/src/Spanner/Connection/LongRunningConnection.php b/src/Spanner/Connection/LongRunningConnection.php
new file mode 100644
index 000000000000..5b75467f25df
--- /dev/null
+++ b/src/Spanner/Connection/LongRunningConnection.php
@@ -0,0 +1,72 @@
+connection = $connection;
+ }
+
+ /**
+ * @param array $args
+ */
+ public function get(array $args)
+ {
+ return $this->connection->getOperation($args);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function cancel(array $args)
+ {
+ return $this->connection->cancelOperation($args);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function delete(array $args)
+ {
+ return $this->connection->deleteOperation($args);
+ }
+
+ /**
+ * @param array $args
+ */
+ public function operations(array $args)
+ {
+ return $this->connection->listOperations($args);
+ }
+}
diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php
new file mode 100644
index 000000000000..8e9667d3847d
--- /dev/null
+++ b/src/Spanner/Database.php
@@ -0,0 +1,1121 @@
+spanner();
+ *
+ * $database = $spanner->connect('my-instance', 'my-database');
+ * ```
+ *
+ * ```
+ * // Databases can also be connected to via an Instance.
+ * use Google\Cloud\ServiceBuilder;
+ *
+ * $cloud = new ServiceBuilder();
+ * $spanner = $cloud->spanner();
+ *
+ * $instance = $spanner->instance('my-instance');
+ * $database = $instance->database('my-database');
+ * ```
+ */
+class Database
+{
+ use LROTrait;
+ use TransactionConfigurationTrait;
+
+ const MAX_RETRIES = 3;
+
+ /**
+ * @var ConnectionInterface
+ */
+ private $connection;
+
+ /**
+ * @var Instance
+ */
+ private $instance;
+
+ /**
+ * @var SessionPoolInterface
+ */
+ private $sessionPool;
+
+ /**
+ * @var LongRunningConnectionInterface
+ */
+ private $lroConnection;
+
+ /**
+ * @var Operation
+ */
+ private $operation;
+
+ /**
+ * @var string
+ */
+ private $projectId;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var Iam
+ */
+ private $iam;
+
+ /**
+ * Create an object representing a Database.
+ *
+ * @param ConnectionInterface $connection The connection to the
+ * Google Cloud Spanner Admin API.
+ * @param Instance $instance The instance in which the database exists.
+ * @param SessionPoolInterface $sessionPool The session pool implementation.
+ * @param LongRunningConnectionInterface $lroConnection An implementation
+ * mapping to methods which handle LRO resolution in the service.
+ * @param string $projectId The project ID.
+ * @param string $name The database name.
+ * @param bool $returnInt64AsObject If true, 64 bit integers will be
+ * returned as a {@see Google\Cloud\Int64} object for 32 bit platform
+ * compatibility. **Defaults to** false.
+ */
+ public function __construct(
+ ConnectionInterface $connection,
+ Instance $instance,
+ SessionPoolInterface $sessionPool,
+ LongRunningConnectionInterface $lroConnection,
+ array $lroCallables,
+ $projectId,
+ $name,
+ $returnInt64AsObject = false
+ ) {
+ $this->connection = $connection;
+ $this->instance = $instance;
+ $this->sessionPool = $sessionPool;
+ $this->lroConnection = $lroConnection;
+ $this->lroCallables = $lroCallables;
+ $this->projectId = $projectId;
+ $this->name = $name;
+
+ $this->operation = new Operation($connection, $returnInt64AsObject);
+ }
+
+ /**
+ * Return the simple database name.
+ *
+ * Example:
+ * ```
+ * $name = $database->name();
+ * ```
+ *
+ * @return string
+ */
+ public function name()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Check if the database exists.
+ *
+ * This method sends a service request.
+ *
+ * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope.
+ *
+ * Example:
+ * ```
+ * if ($database->exists()) {
+ * echo 'Database exists!';
+ * }
+ * ```
+ *
+ * @param array $options [optional] Configuration options.
+ * @return bool
+ */
+ public function exists(array $options = [])
+ {
+ try {
+ $this->ddl($options);
+ } catch (NotFoundException $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Update the Database schema by running a SQL statement.
+ *
+ * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope.
+ *
+ * Example:
+ * ```
+ * $database->updateDdl(
+ * 'CREATE TABLE Users (
+ * id INT64 NOT NULL,
+ * name STRING(100) NOT NULL
+ * password STRING(100) NOT NULL
+ * )'
+ * );
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.UpdateDatabaseDdlRequest UpdateDDLRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $statement A DDL statements to run against a database.
+ * @param array $options [optional] Configuration options.
+ * @return LongRunningOperation
+ */
+ public function updateDdl($statement, array $options = [])
+ {
+ return $this->updateDdlBatch([$statement], $options);
+ }
+
+ /**
+ * Update the Database schema by running a set of SQL statements.
+ *
+ * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope.
+ *
+ * Example:
+ * ```
+ * $database->updateDdlBatch([
+ * 'CREATE TABLE Users (
+ * id INT64 NOT NULL,
+ * name STRING(100) NOT NULL,
+ * password STRING(100) NOT NULL
+ * ) PRIMARY KEY (id)',
+ * 'CREATE TABLE Posts (
+ * id INT64 NOT NULL,
+ * title STRING(100) NOT NULL,
+ * content STRING(MAX) NOT NULL
+ * ) PRIMARY KEY(id)'
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.UpdateDatabaseDdlRequest UpdateDDLRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string[] $statements A list of DDL statements to run against a database.
+ * @param array $options [optional] Configuration options.
+ * @return LongRunningOperation
+ */
+ public function updateDdlBatch(array $statements, array $options = [])
+ {
+ $operation = $this->connection->updateDatabase($options + [
+ 'name' => $this->fullyQualifiedDatabaseName(),
+ 'statements' => $statements,
+ ]);
+
+ return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables);
+ }
+
+ /**
+ * Drop the database.
+ *
+ * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope.
+ *
+ * Example:
+ * ```
+ * $database->drop();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DropDatabaseRequest DropDatabaseRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] Configuration options.
+ * @return void
+ */
+ public function drop(array $options = [])
+ {
+ $this->connection->dropDatabase($options + [
+ 'name' => $this->fullyQualifiedDatabaseName()
+ ]);
+ }
+
+ /**
+ * Get a list of all database DDL statements.
+ *
+ * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope.
+ *
+ * Example:
+ * ```
+ * $statements = $database->ddl();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#getdatabaseddlrequest GetDatabaseDdlRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] Configuration options.
+ * @return array
+ */
+ public function ddl(array $options = [])
+ {
+ $ddl = $this->connection->getDatabaseDDL($options + [
+ 'name' => $this->fullyQualifiedDatabaseName()
+ ]);
+
+ if (isset($ddl['statements'])) {
+ return $ddl['statements'];
+ }
+
+ return [];
+ }
+
+ /**
+ * Manage the database IAM policy
+ *
+ * Example:
+ * ```
+ * $iam = $database->iam();
+ * ```
+ *
+ * @return Iam
+ */
+ public function iam()
+ {
+ if (!$this->iam) {
+ $this->iam = new Iam(
+ new IamDatabase($this->connection),
+ $this->fullyQualifiedDatabaseName()
+ );
+ }
+
+ return $this->iam;
+ }
+
+ /**
+ * Create a snapshot to read from a database at a point in time.
+ *
+ * If no configuration options are provided, transaction will be opened with
+ * strong consistency.
+ *
+ * Snapshots are executed behind the scenes using a Read-Only Transaction.
+ *
+ * Example:
+ * ```
+ * $snapshot = $database->snapshot();
+ * ```
+ *
+ * ```
+ * // Take a shapshot with a returned timestamp.
+ * $snapshot = $database->snapshot([
+ * 'returnReadTimestamp' => true
+ * ]);
+ *
+ * $timestamp = $snapshot->readTimestamp();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest
+ * @see https://cloud.google.com/spanner/docs/transactions Transactions
+ *
+ * @param array $options [optional] {
+ * Configuration Options
+ *
+ * See [ReadOnly](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions.ReadOnly)
+ * for detailed description of available options.
+ *
+ * Please note that only one of `$strong`, `$readTimestamp` or
+ * `$exactStaleness` may be set in a request.
+ *
+ * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected
+ * read timestamp is included in the Transaction message that
+ * describes the transaction.
+ * @type bool $strong Read at a timestamp where all previously committed
+ * transactions are visible.
+ * @type Timestamp $readTimestamp Executes all reads at the given
+ * timestamp.
+ * @type Duration $exactStaleness Represents a number of seconds. Executes
+ * all reads at a timestamp that is $exactStaleness old.
+ * }
+ * @return Snapshot
+ * @codingStandardsIgnoreEnd
+ */
+ public function snapshot(array $options = [])
+ {
+ // These are only available in single-use transactions.
+ if (isset($options['maxStaleness']) || isset($options['minReadTimestamp'])) {
+ throw new \BadMethodCallException(
+ 'maxStaleness and minReadTimestamp are only available in single-use transactions.'
+ );
+ }
+
+ $transactionOptions = $this->configureSnapshotOptions($options);
+
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ);
+
+ return $this->operation->snapshot($session, $transactionOptions);
+ }
+
+ /**
+ * Execute Read/Write operations inside a Transaction.
+ *
+ * Using this method and providing a callable operation provides certain
+ * benefits including automatic retry when a transaction fails. In case of a
+ * failure, all transaction operations, including reads, are re-applied in a
+ * new transaction.
+ *
+ * If a transaction exceeds the maximum number of retries,
+ * {@see Google\Cloud\Exception\AbortedException} will be thrown. Any other
+ * exception types will immediately bubble up and will interrupt the retry
+ * operation.
+ *
+ * Please note that once a transaction reads data, it will lock the read
+ * data, preventing other users from modifying that data. For this reason,
+ * it is important that every transaction commits or rolls back as early as
+ * possible. Do not hold transactions open longer than necessary.
+ *
+ * If you have an active transaction which was obtained from elsewhere, you
+ * can provide it to this method and gain the benefits of managed retry by
+ * setting `$options.transaction` to your {@see Google\Cloud\Spanner\Transaction}
+ * instance. Please note that in this case, it is important that ALL reads
+ * and mutations MUST be performed within the runTransaction callable.
+ *
+ * Example:
+ * ```
+ * $transaction = $database->runTransaction(function (Transaction $t) use ($userName, $password) {
+ * $user = $t->execute('SELECT * FROM Users WHERE Name = @name and PasswordHash = @password', [
+ * 'parameters' => [
+ * 'name' => $userName,
+ * 'password' => password_hash($password)
+ * ]
+ * ])->firstRow();
+ *
+ * if ($user) {
+ * grantAccess($user);
+ *
+ * $user['loginCount'] = $user['loginCount'] + 1;
+ * $t->update('Users', $user);
+ * } else {
+ * $t->rollback();
+ * }
+ *
+ * $t->commit();
+ * });
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest
+ * @see https://cloud.google.com/spanner/docs/transactions Transactions
+ * @codingStandardsIgnoreEnd
+ *
+ * @param callable $operation The operations to run in the transaction.
+ * **Signature:** `function (Transaction $transaction)`.
+ * @param array $options [optional] {
+ * Configuration Options
+ *
+ * @type int $maxRetries The number of times to attempt to apply the
+ * operation before failing. **Defaults to ** `3`.
+ * @type Transaction $transaction If provided, the transaction will be
+ * passed to the callable instead of attempting to begin a new
+ * transaction.
+ * }
+ * @return mixed The return value of `$operation`.
+ */
+ public function runTransaction(callable $operation, array $options = [])
+ {
+ $options += [
+ 'maxRetries' => self::MAX_RETRIES,
+ 'transaction' => null
+ ];
+
+ // There isn't anything configurable here.
+ $options['transactionOptions'] = $this->configureTransactionOptions();
+
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);
+
+ $attempt = 0;
+ $startTransactionFn = function ($session, $options) use ($options, &$attempt) {
+ if ($attempt === 0 && $options['transaction'] instanceof Transaction) {
+ $transaction = $options['transaction'];
+ } else {
+ $transaction = $this->operation->transaction($session, $options);
+ }
+
+ $attempt++;
+ return $transaction;
+ };
+
+ $delayFn = function (\Exception $e) {
+ if (!($e instanceof AbortedException)) {
+ throw $e;
+ }
+
+ $delay = $e->getRetryDelay();
+ time_nanosleep($delay['seconds'], $delay['nanos']);
+ };
+
+ $commitFn = function ($operation, $session, $options) use ($startTransactionFn) {
+ $transaction = call_user_func_array($startTransactionFn, [
+ $session,
+ $options
+ ]);
+
+ return call_user_func($operation, $transaction);
+ };
+
+ $retry = new Retry($options['maxRetries'], $delayFn);
+ return $retry->execute($commitFn, [$operation, $session, $options]);
+ }
+
+ /**
+ * Create and return a new read/write Transaction.
+ *
+ * When manually using a Transaction, it is advised that retry logic be
+ * implemented to reapply all operations when an instance of
+ * {@see Google\Cloud\Exception\AbortedException} is thrown.
+ *
+ * If you wish Google Cloud PHP to handle retry logic for you (recommended
+ * for most cases), use {@see Google\Cloud\Spanner\Database::runTransaction()}.
+ *
+ * Please note that once a transaction reads data, it will lock the read
+ * data, preventing other users from modifying that data. For this reason,
+ * it is important that every transaction commits or rolls back as early as
+ * possible. Do not hold transactions open longer than necessary.
+ *
+ * Example:
+ * ```
+ * $transaction = $database->transaction();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest
+ * @see https://cloud.google.com/spanner/docs/transactions Transactions
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] Configuration Options.
+ * @return Transaction
+ */
+ public function transaction(array $options = [])
+ {
+ // There isn't anything configurable here.
+ $options['transactionOptions'] = [
+ 'readWrite' => []
+ ];
+
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);
+ return $this->operation->transaction($session, $options);
+ }
+
+ /**
+ * Insert a row.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $database->insert('Posts', [
+ * 'ID' => 1337,
+ * 'postTitle' => 'Hello World!',
+ * 'postContent' => 'Welcome to our site.'
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param array $data The row data to insert.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function insert($table, array $data, array $options = [])
+ {
+ return $this->insertBatch($table, [$data], $options);
+ }
+
+ /**
+ * Insert multiple rows.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $database->insert('Posts', [
+ * [
+ * 'ID' => 1337,
+ * 'postTitle' => 'Hello World!',
+ * 'postContent' => 'Welcome to our site.'
+ * ], [
+ * 'ID' => 1338,
+ * 'postTitle' => 'Our History',
+ * 'postContent' => 'Lots of people ask about where we got started.'
+ * ]
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param array $dataSet The row data to insert.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function insertBatch($table, array $dataSet, array $options = [])
+ {
+ $mutations = [];
+ foreach ($dataSet as $data) {
+ $mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data);
+ }
+
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);
+
+ $options['singleUseTransaction'] = $this->configureTransactionOptions();
+ return $this->operation->commit($session, $mutations, $options);
+ }
+
+ /**
+ * Update a row.
+ *
+ * Only data which you wish to update need be included. You must provide
+ * enough information for the API to determine which row should be modified.
+ * In most cases, this means providing values for the Primary Key fields.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $database->update('Posts', [
+ * 'ID' => 1337,
+ * 'postContent' => 'Thanks for visiting our site!'
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param array $data The row data to update.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function update($table, array $data, array $options = [])
+ {
+ return $this->updateBatch($table, [$data], $options);
+ }
+
+ /**
+ * Update multiple rows.
+ *
+ * Only data which you wish to update need be included. You must provide
+ * enough information for the API to determine which row should be modified.
+ * In most cases, this means providing values for the Primary Key fields.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $database->update('Posts', [
+ * [
+ * 'ID' => 1337,
+ * 'postContent' => 'Thanks for visiting our site!'
+ * ], [
+ * 'ID' => 1338,
+ * 'postContent' => 'A little bit about us!'
+ * ]
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param array $dataSet The row data to update.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function updateBatch($table, array $dataSet, array $options = [])
+ {
+ $mutations = [];
+ foreach ($dataSet as $data) {
+ $mutations[] = $this->operation->mutation(Operation::OP_UPDATE, $table, $data);
+ }
+
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);
+
+ $options['singleUseTransaction'] = $this->configureTransactionOptions();
+ return $this->operation->commit($session, $mutations, $options);
+ }
+
+ /**
+ * Insert or update a row.
+ *
+ * If a row already exists (determined by comparing the Primary Key to
+ * existing table data), the row will be updated. If not, it will be
+ * created.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $database->insertOrUpdate('Posts', [
+ * 'ID' => 1337,
+ * 'postTitle' => 'Hello World!',
+ * 'postContent' => 'Thanks for visiting our site!'
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param array $data The row data to insert or update.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function insertOrUpdate($table, array $data, array $options = [])
+ {
+ return $this->insertOrUpdateBatch($table, [$data], $options);
+ }
+
+ /**
+ * Insert or update multiple rows.
+ *
+ * If a row already exists (determined by comparing the Primary Key to
+ * existing table data), the row will be updated. If not, it will be
+ * created.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $database->insertOrUpdateBatch('Posts', [
+ * [
+ * 'ID' => 1337,
+ * 'postTitle' => 'Hello World!',
+ * 'postContent' => 'Thanks for visiting our site!'
+ * ], [
+ * 'ID' => 1338,
+ * 'postTitle' => 'Our History',
+ * 'postContent' => 'A little bit about us!'
+ * ]
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param array $dataSet The row data to insert or update.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function insertOrUpdateBatch($table, array $dataSet, array $options = [])
+ {
+ $mutations = [];
+ foreach ($dataSet as $data) {
+ $mutations[] = $this->operation->mutation(Operation::OP_INSERT_OR_UPDATE, $table, $data);
+ }
+
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);
+
+ $options['singleUseTransaction'] = $this->configureTransactionOptions();
+ return $this->operation->commit($session, $mutations, $options);
+ }
+
+ /**
+ * Replace a row.
+ *
+ * Provide data for the entire row. Google Cloud Spanner will attempt to
+ * find a record matching the Primary Key, and will replace the entire row.
+ * If a matching row is not found, it will be inserted.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $database->replace('Posts', [
+ * 'ID' => 1337,
+ * 'postTitle' => 'Hello World!',
+ * 'postContent' => 'Thanks for visiting our site!'
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param array $data The row data to replace.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function replace($table, array $data, array $options = [])
+ {
+ return $this->replaceBatch($table, [$data], $options);
+ }
+
+ /**
+ * Replace multiple rows.
+ *
+ * Provide data for the entire row. Google Cloud Spanner will attempt to
+ * find a record matching the Primary Key, and will replace the entire row.
+ * If a matching row is not found, it will be inserted.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $database->replaceBatch('Posts', [
+ * [
+ * 'ID' => 1337,
+ * 'postTitle' => 'Hello World!',
+ * 'postContent' => 'Thanks for visiting our site!'
+ * ], [
+ * 'ID' => 1338,
+ * 'postTitle' => 'Our History',
+ * 'postContent' => 'A little bit about us!'
+ * ]
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param array $dataSet The row data to replace.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function replaceBatch($table, array $dataSet, array $options = [])
+ {
+ $mutations = [];
+ foreach ($dataSet as $data) {
+ $mutations[] = $this->operation->mutation(Operation::OP_REPLACE, $table, $data);
+ }
+
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);
+
+ $options['singleUseTransaction'] = $this->configureTransactionOptions();
+ return $this->operation->commit($session, $mutations, $options);
+ }
+
+ /**
+ * Delete one or more rows.
+ *
+ * Mutations are committed in a single-use transaction.
+ *
+ * Example:
+ * ```
+ * $keySet = $spanner->keySet([
+ * 'keys' => [
+ * 1337, 1338
+ * ]
+ * ]);
+ *
+ * $database->delete('Posts', $keySet);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $table The table to mutate.
+ * @param KeySet $keySet The KeySet to identify rows to delete.
+ * @param array $options [optional] Configuration options.
+ * @return Timestamp The commit Timestamp.
+ */
+ public function delete($table, KeySet $keySet, array $options = [])
+ {
+ $mutations = [$this->operation->deleteMutation($table, $keySet)];
+
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);
+
+ $options['singleUseTransaction'] = $this->configureTransactionOptions();
+ return $this->operation->commit($session, $mutations, $options);
+ }
+
+ /**
+ * Run a query.
+ *
+ * Example:
+ * ```
+ * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [
+ * 'parameters' => [
+ * 'postId' => 1337
+ * ]
+ * ]);
+ * ```
+ *
+ * ```
+ * // Execute a read and return a new Snapshot for further reads.
+ * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [
+ * 'parameters' => [
+ * 'postId' => 1337
+ * ],
+ * 'begin' => true
+ * ]);
+ *
+ * $snapshot = $result->snapshot();
+ * ```
+ *
+ * ```
+ * // Execute a read and return a new Transaction for further reads and writes.
+ * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [
+ * 'parameters' => [
+ * 'postId' => 1337
+ * ],
+ * 'begin' => true,
+ * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE
+ * ]);
+ *
+ * $transaction = $result->transaction();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ExecuteSqlRequest ExecuteSqlRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @codingStandardsIgnoreStart
+ * @param string $sql The query string to execute.
+ * @param array $options [optional] {
+ * Configuration Options.
+ *
+ * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions)
+ * for detailed description of available transaction options.
+ *
+ * Please note that only one of `$strong`, `$minReadTimestamp`,
+ * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in
+ * a request.
+ *
+ * @type array $parameters A key/value array of Query Parameters, where
+ * the key is represented in the query string prefixed by a `@`
+ * symbol.
+ * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected
+ * read timestamp is included in the Transaction message that
+ * describes the transaction.
+ * @type bool $strong Read at a timestamp where all previously committed
+ * transactions are visible.
+ * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the
+ * given timestamp. Only available in single-use transactions.
+ * @type Duration $maxStaleness Read data at a timestamp >= NOW - the
+ * given timestamp. Only available in single-use transactions.
+ * @type Timestamp $readTimestamp Executes all reads at the given
+ * timestamp.
+ * @type Duration $exactStaleness Represents a number of seconds. Executes
+ * all reads at a timestamp that is $exactStaleness old.
+ * @type bool $begin If true, will begin a new transaction. If a
+ * read/write transaction is desired, set the value of
+ * $transactionType. If a transaction or snapshot is created, it
+ * will be returned as `$result->transaction()` or
+ * `$result->snapshot()`. **Defaults to** `false`.
+ * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ`
+ * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is
+ * chosen, any snapshot options will be disregarded. If `$begin`
+ * is false, this option will be ignored. **Defaults to**
+ * `SessionPoolInterface::CONTEXT_READ`.
+ * }
+ * @codingStandardsIgnoreEnd
+ * @return Result
+ */
+ public function execute($sql, array $options = [])
+ {
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ);
+
+ list($transactionOptions, $context) = $this->transactionSelector($options);
+ $options['transaction'] = $transactionOptions;
+ $options['transactionContext'] = $context;
+
+ return $this->operation->execute($session, $sql, $options);
+ }
+
+ /**
+ * Lookup rows in a table.
+ *
+ * Note that if no KeySet is specified, all rows in a table will be
+ * returned.
+ *
+ * Example:
+ * ```
+ * $keySet = $spanner->keySet([
+ * 'keys' => [1337]
+ * ]);
+ *
+ * $columns = ['ID', 'title', 'content'];
+ *
+ * $result = $database->read('Posts', $keySet, $columns);
+ * ```
+ *
+ * ```
+ * // Execute a read and return a new Snapshot for further reads.
+ * $keySet = $spanner->keySet([
+ * 'keys' => [1337]
+ * ]);
+ *
+ * $columns = ['ID', 'title', 'content'];
+ *
+ * $result = $database->read('Posts', $keySet, $columns, [
+ * 'begin' => true
+ * ]);
+ *
+ * $snapshot = $result->snapshot();
+ * ```
+ *
+ * ```
+ * // Execute a read and return a new Transaction for further reads and writes.
+ * $keySet = $spanner->keySet([
+ * 'keys' => [1337]
+ * ]);
+ *
+ * $columns = ['ID', 'title', 'content'];
+ *
+ * $result = $database->read('Posts', $keySet, $columns, [
+ * 'begin' => true,
+ * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE
+ * ]);
+ *
+ * $transaction = $result->transaction();
+ * ```
+ *
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest ReadRequest
+ *
+ * @codingStandardsIgnoreStart
+ * @param string $table The table name.
+ * @param KeySet $keySet The KeySet to select rows.
+ * @param array $columns A list of column names to return.
+ * @param array $options [optional] {
+ * Configuration Options.
+ *
+ * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions)
+ * for detailed description of available transaction options.
+ *
+ * Please note that only one of `$strong`, `$minReadTimestamp`,
+ * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in
+ * a request.
+ *
+ * @type string $index The name of an index on the table.
+ * @type int $offset The number of rows to offset results by.
+ * @type int $limit The number of results to return.
+ * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected
+ * read timestamp is included in the Transaction message that
+ * describes the transaction.
+ * @type bool $strong Read at a timestamp where all previously committed
+ * transactions are visible.
+ * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the
+ * given timestamp. Only available in single-use transactions.
+ * @type Duration $maxStaleness Read data at a timestamp >= NOW - the
+ * given timestamp. Only available in single-use transactions.
+ * @type Timestamp $readTimestamp Executes all reads at the given
+ * timestamp.
+ * @type Duration $exactStaleness Represents a number of seconds. Executes
+ * all reads at a timestamp that is $exactStaleness old.
+ * @type bool $begin If true, will begin a new transaction. If a
+ * read/write transaction is desired, set the value of
+ * $transactionType. If a transaction or snapshot is created, it
+ * will be returned as `$result->transaction()` or
+ * `$result->snapshot()`. **Defaults to** `false`.
+ * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ`
+ * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is
+ * chosen, any snapshot options will be disregarded. If `$begin`
+ * is false, this option will be ignored. **Defaults to**
+ * `SessionPoolInterface::CONTEXT_READ`.
+ * }
+ * @codingStandardsIgnoreEnd
+ * @return Result
+ */
+ public function read($table, KeySet $keySet, array $columns, array $options = [])
+ {
+ $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ);
+
+ list($transactionOptions, $context) = $this->transactionSelector($options);
+ $options['transaction'] = $transactionOptions;
+ $options['transactionContext'] = $context;
+
+ return $this->operation->read($session, $table, $keySet, $columns, $options);
+ }
+
+ /**
+ * Retrieve a session from the session pool.
+ *
+ * @param string $context The session context.
+ * @return Session
+ */
+ private function selectSession($context = SessionPoolInterface::CONTEXT_READ)
+ {
+ return $this->sessionPool->session(
+ $this->instance->name(),
+ $this->name,
+ $context
+ );
+ }
+
+ /**
+ * Convert the simple database name to a fully qualified name.
+ *
+ * @return string
+ */
+ private function fullyQualifiedDatabaseName()
+ {
+ return GrpcSpannerClient::formatDatabaseName(
+ $this->projectId,
+ $this->instance->name(),
+ $this->name
+ );
+ }
+
+ /**
+ * Represent the class in a more readable and digestable fashion.
+ *
+ * @access private
+ * @codeCoverageIgnore
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'connection' => get_class($this->connection),
+ 'projectId' => $this->projectId,
+ 'name' => $this->name,
+ 'instance' => $this->instance,
+ 'sessionPool' => $this->sessionPool,
+ ];
+ }
+}
diff --git a/src/Spanner/Date.php b/src/Spanner/Date.php
new file mode 100644
index 000000000000..777f18b48840
--- /dev/null
+++ b/src/Spanner/Date.php
@@ -0,0 +1,111 @@
+spanner();
+ *
+ * $date = $spanner->date(new \DateTime('1995-02-04'));
+ * ```
+ *
+ * ```
+ * // Date objects can be cast to strings for easy display.
+ * echo (string) $date;
+ * ```
+ */
+class Date implements ValueInterface
+{
+ const FORMAT = 'Y-m-d';
+
+ /**
+ * @var \DateTimeInterface
+ */
+ protected $value;
+
+ /**
+ * @param \DateTimeInterface $value The date value.
+ */
+ public function __construct(\DateTimeInterface $value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Get the underlying `\DateTimeInterface` implementation.
+ *
+ * Example:
+ * ```
+ * $dateTime = $date->get();
+ * ```
+ *
+ * @return \DateTimeInterface
+ */
+ public function get()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Get the type.
+ *
+ * Example:
+ * ```
+ * echo $date->type();
+ * ```
+ *
+ * @return string
+ */
+ public function type()
+ {
+ return ValueMapper::TYPE_DATE;
+ }
+
+ /**
+ * Format the value as a string.
+ *
+ * Example:
+ * ```
+ * echo $date->formatAsString();
+ * ```
+ *
+ * @return string
+ */
+ public function formatAsString()
+ {
+ return $this->value->format(self::FORMAT);
+ }
+
+ /**
+ * Format the value as a string.
+ *
+ * @return string
+ * @access private
+ */
+ public function __toString()
+ {
+ return $this->formatAsString();
+ }
+}
diff --git a/src/Spanner/Duration.php b/src/Spanner/Duration.php
new file mode 100644
index 000000000000..d8c0b45c510d
--- /dev/null
+++ b/src/Spanner/Duration.php
@@ -0,0 +1,122 @@
+spanner();
+ *
+ * $seconds = 100;
+ * $nanoSeconds = 000001;
+ * $duration = $spanner->duration($seconds, $nanoSeconds);
+ * ```
+ *
+ * ```
+ * // Duration objects can be cast to strings for easy display.
+ * echo (string) $duration;
+ * ```
+ */
+class Duration implements ValueInterface
+{
+ const TYPE = 'DURATION';
+
+ /**
+ * @var int
+ */
+ private $seconds;
+
+ /**
+ * @var int
+ */
+ private $nanos;
+
+ /**
+ * @param int $seconds The number of seconds in the duration.
+ * @param int $nanos The number of nanoseconds in the duration.
+ */
+ public function __construct($seconds, $nanos = 0)
+ {
+ $this->seconds = $seconds;
+ $this->nanos = $nanos;
+ }
+
+ /**
+ * Get the duration
+ *
+ * Example:
+ * ```
+ * $res = $duration->get();
+ * ```
+ *
+ * @return array
+ */
+ public function get()
+ {
+ return [
+ 'seconds' => $this->seconds,
+ 'nanos' => $this->nanos
+ ];
+ }
+
+ /**
+ * Get the type.
+ *
+ * Example:
+ * ```
+ * echo $duration->type();
+ * ```
+ *
+ * @return string
+ */
+ public function type()
+ {
+ return self::TYPE;
+ }
+
+ /**
+ * Format the value as a string.
+ *
+ * Example:
+ * ```
+ * echo $date->formatAsString();
+ * ```
+ *
+ * @return string
+ */
+ public function formatAsString()
+ {
+ return json_encode($this->get());
+ }
+
+ /**
+ * Format the value as a string.
+ *
+ * @return string
+ * @access private
+ */
+ public function __toString()
+ {
+ return $this->formatAsString();
+ }
+}
diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php
new file mode 100644
index 000000000000..1f3493f20bf4
--- /dev/null
+++ b/src/Spanner/Instance.php
@@ -0,0 +1,471 @@
+spanner();
+ *
+ * $instance = $spanner->instance('my-instance');
+ * ```
+ */
+class Instance
+{
+ use ArrayTrait;
+ use LROTrait;
+
+ const STATE_READY = State::READY;
+ const STATE_CREATING = State::CREATING;
+
+ /**
+ * @var ConnectionInterface
+ */
+ private $connection;
+
+ /**
+ * @var SessionPool;
+ */
+ private $sessionPool;
+
+ /**
+ * @var LongRunningConnectionInterface
+ */
+ private $lroConnection;
+
+ /**
+ * @var array
+ */
+ private $lroCallables;
+
+ /**
+ * @var string
+ */
+ private $projectId;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var bool
+ */
+ private $returnInt64AsObject;
+
+ /**
+ * @var array
+ */
+ private $info;
+
+ /**
+ * @var Iam
+ */
+ private $iam;
+
+ /**
+ * Create an object representing a Google Cloud Spanner instance.
+ *
+ * @param ConnectionInterface $connection The connection to the
+ * Google Cloud Spanner Admin API.
+ * @param SessionPoolInterface $sessionPool The session pool implementation.
+ * @param LongRunningConnectionInterface $lroConnection An implementation
+ * mapping to methods which handle LRO resolution in the service.
+ * @param array $lroCallables
+ * @param string $projectId The project ID.
+ * @param string $name The instance name.
+ * @param bool $returnInt64AsObject If true, 64 bit integers will be
+ * returned as a {@see Google\Cloud\Int64} object for 32 bit platform
+ * compatibility. **Defaults to** false.
+ * @param array $info [optional] A representation of the instance object.
+ */
+ public function __construct(
+ ConnectionInterface $connection,
+ SessionPoolInterface $sessionPool,
+ LongRunningConnectionInterface $lroConnection,
+ array $lroCallables,
+ $projectId,
+ $name,
+ $returnInt64AsObject = false,
+ array $info = []
+ ) {
+ $this->connection = $connection;
+ $this->sessionPool = $sessionPool;
+ $this->lroConnection = $lroConnection;
+ $this->lroCallables = $lroCallables;
+ $this->projectId = $projectId;
+ $this->name = $name;
+ $this->returnInt64AsObject = $returnInt64AsObject;
+ $this->info = $info;
+ }
+
+ /**
+ * Return the instance name.
+ *
+ * Example:
+ * ```
+ * $name = $instance->name();
+ * ```
+ *
+ * @return string
+ */
+ public function name()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Return the service representation of the instance.
+ *
+ * This method may require a service call.
+ *
+ * Example:
+ * ```
+ * $info = $instance->info();
+ * echo $info['nodeCount'];
+ * ```
+ *
+ * @param array $options [optional] Configuration options.
+ * @return array
+ */
+ public function info(array $options = [])
+ {
+ if (!$this->info) {
+ $this->reload($options);
+ }
+
+ return $this->info;
+ }
+
+ /**
+ * Check if the instance exists.
+ *
+ * This method requires a service call.
+ *
+ * Example:
+ * ```
+ * if ($instance->exists()) {
+ * echo 'Instance exists!';
+ * }
+ * ```
+ *
+ * @param array $options [optional] Configuration options.
+ * @return array
+ */
+ public function exists(array $options = [])
+ {
+ try {
+ $this->reload($options = []);
+ } catch (NotFoundException $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Fetch a fresh representation of the instance from the service.
+ *
+ * Example:
+ * ```
+ * $info = $instance->reload();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.GetInstanceRequest GetInstanceRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] Configuration options.
+ * @return array
+ */
+ public function reload(array $options = [])
+ {
+ $this->info = $this->connection->getInstance($options + [
+ 'name' => $this->fullyQualifiedInstanceName()
+ ]);
+
+ return $this->info;
+ }
+
+ /**
+ * Return the instance state.
+ *
+ * When instances are created or updated, they may take some time before
+ * they are ready for use. This method allows for checking whether an
+ * instance is ready.
+ *
+ * Example:
+ * ```
+ * if ($instance->state() === Instance::STATE_READY) {
+ * echo 'Instance is ready!';
+ * }
+ * ```
+ *
+ * @param array $options [optional] Configuration options.
+ * @return string
+ */
+ public function state(array $options = [])
+ {
+ $info = $this->info($options);
+
+ return (isset($info['state']))
+ ? $info['state']
+ : null;
+ }
+
+ /**
+ * Update the instance
+ *
+ * Example:
+ * ```
+ * $instance->update([
+ * 'displayName' => 'My Instance',
+ * 'nodeCount' => 4
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#updateinstancerequest UpdateInstanceRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] {
+ * Configuration options
+ *
+ * @type string $displayName The descriptive name for this instance as
+ * it appears in UIs. **Defaults to** the value of $name.
+ * @type int $nodeCount The number of nodes allocated to this instance.
+ * **Defaults to** `1`.
+ * @type array $labels For more information, see
+ * [Using labels to organize Google Cloud Platform resources](https://goo.gl/xmQnxf).
+ * }
+ * @return LongRunningOperation
+ * @throws \InvalidArgumentException
+ */
+ public function update(array $options = [])
+ {
+ $info = $this->info($options);
+
+ $options += [
+ 'displayName' => $info['displayName'],
+ 'nodeCount' => (isset($info['nodeCount'])) ? $info['nodeCount'] : null,
+ 'labels' => (isset($info['labels']))
+ ? $info['labels']
+ : []
+ ];
+
+ $operation = $this->connection->updateInstance([
+ 'name' => $this->fullyQualifiedInstanceName(),
+ ] + $options);
+
+ return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables);
+ }
+
+ /**
+ * Delete the instance, any databases in the instance, and all data.
+ *
+ * Example:
+ * ```
+ * $instance->delete();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#deleteinstancerequest DeleteInstanceRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] Configuration options.
+ * @return void
+ */
+ public function delete(array $options = [])
+ {
+ return $this->connection->deleteInstance($options + [
+ 'name' => $this->fullyQualifiedInstanceName()
+ ]);
+ }
+
+ /**
+ * Create a Database
+ *
+ * Example:
+ * ```
+ * $database = $instance->createDatabase('my-database');
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#createdatabaserequest CreateDatabaseRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $name The database name.
+ * @param array $options [optional] {
+ * Configuration Options
+ *
+ * @type array $statements Additional DDL statements.
+ * }
+ * @return LongRunningOperation
+ */
+ public function createDatabase($name, array $options = [])
+ {
+ $options += [
+ 'statements' => [],
+ ];
+
+ $statement = sprintf('CREATE DATABASE `%s`', $name);
+
+ $operation = $this->connection->createDatabase([
+ 'instance' => $this->fullyQualifiedInstanceName(),
+ 'createStatement' => $statement,
+ 'extraStatements' => $options['statements']
+ ]);
+
+ return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables);
+ }
+
+ /**
+ * Lazily instantiate a database object
+ *
+ * Example:
+ * ```
+ * $database = $instance->database('my-database');
+ * ```
+ *
+ * @param string $name The database name
+ * @return Database
+ */
+ public function database($name)
+ {
+ return new Database(
+ $this->connection,
+ $this,
+ $this->sessionPool,
+ $this->lroConnection,
+ $this->lroCallables,
+ $this->projectId,
+ $name,
+ $this->returnInt64AsObject
+ );
+ }
+
+ /**
+ * List databases in an instance
+ *
+ * Example:
+ * ```
+ * $databases = $instance->databases();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.database.v1#listdatabasesrequest ListDatabasesRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] {
+ * Configuration options.
+ *
+ * @type int $pageSize Maximum number of results to return per
+ * request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
+ * }
+ * @return ItemIterator
+ */
+ public function databases(array $options = [])
+ {
+ $resultLimit = $this->pluck('resultLimit', $options, false);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $database) {
+ $name = DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']);
+ return $this->database($name);
+ },
+ [$this->connection, 'listDatabases'],
+ $options + ['instance' => $this->fullyQualifiedInstanceName()],
+ [
+ 'itemsKey' => 'databases',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
+ }
+
+ /**
+ * Manage the instance IAM policy
+ *
+ * Example:
+ * ```
+ * $iam = $instance->iam();
+ * ```
+ *
+ * @return Iam
+ */
+ public function iam()
+ {
+ if (!$this->iam) {
+ $this->iam = new Iam(
+ new IamInstance($this->connection),
+ $this->fullyQualifiedInstanceName()
+ );
+ }
+
+ return $this->iam;
+ }
+
+ /**
+ * Convert the simple instance name to a fully qualified name.
+ *
+ * @return string
+ */
+ private function fullyQualifiedInstanceName()
+ {
+ return InstanceAdminClient::formatInstanceName($this->projectId, $this->name);
+ }
+
+ /**
+ * Represent the class in a more readable and digestable fashion.
+ *
+ * @access private
+ * @codeCoverageIgnore
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'connection' => get_class($this->connection),
+ 'projectId' => $this->projectId,
+ 'name' => $this->name,
+ 'info' => $this->info
+ ];
+ }
+}
diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php
new file mode 100644
index 000000000000..4407745f00ac
--- /dev/null
+++ b/src/Spanner/KeyRange.php
@@ -0,0 +1,226 @@
+spanner();
+ *
+ * // Create a KeyRange for all people named Bob, born in 1969.
+ * $start = $spanner->date(new \DateTime('1969-01-01'));
+ * $end = $spanner->date(new \DateTime('1969-12-31'));
+ *
+ * $range = $spanner->keyRange([
+ * 'startType' => KeyRange::TYPE_CLOSED,
+ * 'start' => ['Bob', $start],
+ * 'endType' => KeyRange::TYPE_CLOSED,
+ * 'end' => ['Bob', $end]
+ * ]);
+ * ```
+ */
+class KeyRange
+{
+ const TYPE_OPEN = 'open';
+ const TYPE_CLOSED = 'closed';
+
+ /**
+ * @var array
+ */
+ private $types = [];
+
+ /**
+ * @var array
+ */
+ private $range = [];
+
+ /**
+ * @var array
+ */
+ private $definition = [
+ self::TYPE_OPEN => [
+ 'start' => 'startOpen',
+ 'end' => 'endOpen'
+ ],
+ self::TYPE_CLOSED => [
+ 'start' => 'startClosed',
+ 'end' => 'endClosed'
+ ]
+ ];
+
+ /**
+ * Create a KeyRange.
+ *
+ * @param array $options [optional] {
+ * Configuration Options.
+ *
+ * @type string $startType Either "open" or "closed". Use constants
+ * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for
+ * guaranteed correctness.
+ * @type array $start The key with which to start the range.
+ * @type string $endType Either "open" or "closed". Use constants
+ * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for
+ * guaranteed correctness.
+ * @type array $end The key with which to end the range.
+ * }
+ */
+ public function __construct(array $options = [])
+ {
+ $options = array_filter($options + [
+ 'startType' => null,
+ 'start' => [],
+ 'endType' => null,
+ 'end' => []
+ ]);
+
+ if (isset($options['startType']) && isset($options['start'])) {
+ $this->setStart($options['startType'], $options['start']);
+ }
+
+ if (isset($options['endType']) && isset($options['end'])) {
+ $this->setEnd($options['endType'], $options['end']);
+ }
+ }
+
+ /**
+ * Get the range start.
+ *
+ * Example:
+ * ```
+ * $start = $range->start();
+ * ```
+ *
+ * @return array
+ */
+ public function start()
+ {
+ $type = $this->types['start'];
+ return $this->range[$this->definition[$type]['start']];
+ }
+
+ /**
+ * Set the range start.
+ *
+ * Example:
+ * ```
+ * $range->setStart(KeyRange::TYPE_OPEN, ['Bob']);
+ * ```
+ *
+ * @param string $type Either "open" or "closed". Use constants
+ * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed
+ * correctness.
+ * @param array $start The start of the key range.
+ * @return void
+ */
+ public function setStart($type, array $start)
+ {
+ if (!in_array($type, array_keys($this->definition))) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Invalid KeyRange type. Allowed values are %s',
+ implode(', ', array_keys($this->definition))
+ ));
+ }
+
+ $rangeKey = $this->definition[$type]['start'];
+
+ $this->types['start'] = $type;
+ $this->range[$rangeKey] = $start;
+ }
+
+ /**
+ * Get the range end.
+ *
+ * Example:
+ * ```
+ * $end = $range->end();
+ * ```
+ *
+ * @return array
+ */
+ public function end()
+ {
+ $type = $this->types['end'];
+ return $this->range[$this->definition[$type]['end']];
+ }
+
+ /**
+ * Set the range end.
+ *
+ * Example:
+ * ```
+ * $range->setEnd(KeyRange::TYPE_CLOSED, ['Jill']);
+ * ```
+ *
+ * @param string $type Either "open" or "closed". Use constants
+ * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed
+ * correctness.
+ * @param array $end The end of the key range.
+ * @return void
+ */
+ public function setEnd($type, array $end)
+ {
+ if (!in_array($type, array_keys($this->definition))) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Invalid KeyRange type. Allowed values are %s',
+ implode(', ', array_keys($this->definition))
+ ));
+ }
+
+ $rangeKey = $this->definition[$type]['end'];
+
+ $this->types['end'] = $type;
+ $this->range[$rangeKey] = $end;
+ }
+
+ /**
+ * Get the start and end types
+ *
+ * Example:
+ * ```
+ * $types = $range->types();
+ * ```
+ *
+ * @return array
+ */
+ public function types()
+ {
+ return $this->types;
+ }
+
+ /**
+ * Returns an API-compliant representation of a KeyRange.
+ *
+ * @return array
+ * @access private
+ */
+ public function keyRangeObject()
+ {
+ if (count($this->range) !== 2) {
+ throw new \BadMethodCallException('Key Range must supply a start and an end');
+ }
+
+ return $this->range;
+ }
+}
diff --git a/src/Spanner/KeySet.php b/src/Spanner/KeySet.php
new file mode 100644
index 000000000000..c13297d6c1d6
--- /dev/null
+++ b/src/Spanner/KeySet.php
@@ -0,0 +1,240 @@
+spanner();
+ *
+ * $keySet = $spanner->keySet();
+ * ```
+ *
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#keyset KeySet
+ */
+class KeySet
+{
+ use ValidateTrait;
+
+ /**
+ * @var array
+ */
+ private $keys;
+
+ /**
+ * @var KeyRange[]
+ */
+ private $ranges;
+
+ /**
+ * @var bool
+ */
+ private $all;
+
+ /**
+ * Create a KeySet.
+ *
+ * @param array $options [optional] {
+ * @type array $keys A list of specific keys. Entries in keys should
+ * have exactly as many elements as there are columns in the
+ * primary or index key with which this KeySet is used.
+ * @type KeyRange[] $ranges A list of Key Ranges.
+ * @type bool $all If true, KeySet will match all keys in a table.
+ * **Defaults to** `false`.
+ * }
+ */
+ public function __construct(array $options = [])
+ {
+ $options += [
+ 'keys' => [],
+ 'ranges' => [],
+ 'all' => false
+ ];
+
+ $this->validateBatch($options['ranges'], KeyRange::class);
+
+ $this->keys = $options['keys'];
+ $this->ranges = $options['ranges'];
+ $this->all = (bool) $options['all'];
+ }
+
+ /**
+ * Fetch the KeyRanges
+ *
+ * Example:
+ * ```
+ * $ranges = $keySet->ranges();
+ * ```
+ *
+ * @return KeyRange[]
+ */
+ public function ranges()
+ {
+ return $this->ranges;
+ }
+
+
+ /**
+ * Add a single KeyRange.
+ *
+ * Example:
+ * ```
+ * $range = $spanner->keyRange();
+ * $keySet->addRange($range);
+ * ```
+ *
+ * @param KeyRange $range A KeyRange instance.
+ * @return void
+ */
+ public function addRange(KeyRange $range)
+ {
+ $this->ranges[] = $range;
+ }
+
+ /**
+ * Set the KeySet's KeyRanges.
+ *
+ * Any existing KeyRanges will be overridden.
+ *
+ * Example:
+ * ```
+ * $range = $spanner->keyRange();
+ * $keySet->setRanges([$range]);
+ * ```
+ *
+ * @param KeyRange[] $ranges An array of KeyRange objects.
+ * @return void
+ */
+ public function setRanges(array $ranges)
+ {
+ $this->validateBatch($ranges, KeyRange::class);
+
+ $this->ranges = $ranges;
+ }
+
+ /**
+ * Fetch the keys.
+ *
+ * Example:
+ * ```
+ * $keys = $keySet->keys();
+ * ```
+ *
+ * @return mixed[]
+ */
+ public function keys()
+ {
+ return $this->keys;
+ }
+
+ /**
+ * Add a single key.
+ *
+ * A Key should have exactly as many elements as there are columns in the
+ * primary or index key with which this KeySet is used.
+ *
+ * Example:
+ * ```
+ * $keySet->addKey('Bob');
+ * ```
+ *
+ * @param mixed $key The Key to add.
+ * @return void
+ */
+ public function addKey($key)
+ {
+ $this->keys[] = $key;
+ }
+
+ /**
+ * Set the KeySet keys.
+ *
+ * Any existing keys will be overridden.
+ *
+ * Example:
+ * ```
+ * $keySet->setKeys(['Bob', 'Jill']);
+ * ```
+ *
+ * @param mixed[] $keys
+ * @return void
+ */
+ public function setKeys(array $keys)
+ {
+ $this->keys = $keys;
+ }
+
+ /**
+ * Get the value of Match All.
+ *
+ * Example:
+ * ```
+ * if ($keySet->matchAll()) {
+ * echo "All keys will match";
+ * }
+ * ```
+ *
+ * @return bool
+ */
+ public function matchAll()
+ {
+ return $this->all;
+ }
+
+ /**
+ * Choose whether the KeySet should match all keys in a table.
+ *
+ * Example:
+ * ```
+ * $keySet->matchAll(true);
+ * ```
+ *
+ * @param bool $all If true, all keys in a table will be matched.
+ * @return void
+ */
+ public function setMatchAll($all)
+ {
+ $this->all = (bool) $all;
+ }
+
+ /**
+ * Format a KeySet object for use in the Spanner API.
+ *
+ * @access private
+ */
+ public function keySetObject()
+ {
+ $ranges = [];
+ foreach ($this->ranges as $range) {
+ $ranges[] = $range->keyRangeObject();
+ }
+
+ return [
+ 'keys' => $this->keys,
+ 'ranges' => $ranges,
+ 'all' => $this->all
+ ];
+ }
+}
diff --git a/src/Spanner/LICENSE b/src/Spanner/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/Spanner/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php
new file mode 100644
index 000000000000..ef32aea444ee
--- /dev/null
+++ b/src/Spanner/Operation.php
@@ -0,0 +1,382 @@
+connection = $connection;
+ $this->mapper = new ValueMapper($returnInt64AsObject);
+ }
+
+ /**
+ * Create a formatted mutation.
+ *
+ * @param string $operation The operation type.
+ * @param string $table The table name.
+ * @param array $mutation The mutation data, represented as a set of
+ * key/value pairs.
+ * @return array
+ */
+ public function mutation($operation, $table, $mutation)
+ {
+ $mutation = $this->arrayFilterRemoveNull($mutation);
+
+ return [
+ $operation => [
+ 'table' => $table,
+ 'columns' => array_keys($mutation),
+ 'values' => $this->mapper->encodeValuesAsSimpleType(array_values($mutation))
+ ]
+ ];
+ }
+
+ /**
+ * Create a formatted delete mutation.
+ *
+ * @param string $table The table name.
+ * @param KeySet $keySet The keys to delete.
+ * @return array
+ */
+ public function deleteMutation($table, KeySet $keySet)
+ {
+ return [
+ self::OP_DELETE => [
+ 'table' => $table,
+ 'keySet' => $this->flattenKeySet($keySet),
+ ]
+ ];
+ }
+
+ /**
+ * Commit all enqueued mutations.
+ *
+ * @codingStandardsIgnoreStart
+ * @param Session $session The session ID to use for the commit.
+ * @param Transaction $transaction The transaction to commit.
+ * @param array $options [optional] {
+ * Configuration options.
+ *
+ * @type string $transactionId
+ * }
+ * @return Timestamp The commit Timestamp.
+ */
+ public function commit(Session $session, array $mutations, array $options = [])
+ {
+ $options += [
+ 'transactionId' => null
+ ];
+
+ $res = $this->connection->commit($this->arrayFilterRemoveNull([
+ 'mutations' => $mutations,
+ 'session' => $session->name()
+ ]) + $options);
+
+ return $this->mapper->createTimestampWithNanos($res['commitTimestamp']);
+ }
+
+ /**
+ * Rollback a Transaction
+ *
+ * @param Session $session The session to use for the rollback.
+ * Note that the session MUST be the same one in which the
+ * transaction was created.
+ * @param string $transactionId The transaction to roll back.
+ * @param array $options [optional] Configuration Options.
+ * @return void
+ */
+ public function rollback(Session $session, $transactionId, array $options = [])
+ {
+ return $this->connection->rollback([
+ 'transactionId' => $transactionId,
+ 'session' => $session->name()
+ ] + $options);
+ }
+
+ /**
+ * Run a query
+ *
+ * @param Session $session The session to use to execute the SQL.
+ * @param string $sql The query string.
+ * @param array $options [optional] Configuration options.
+ * @return array
+ */
+ public function execute(Session $session, $sql, array $options = [])
+ {
+ $options += [
+ 'parameters' => [],
+ 'transactionContext' => null
+ ];
+
+ $parameters = $this->pluck('parameters', $options);
+ $options += $this->mapper->formatParamsForExecuteSql($parameters);
+
+ $context = $this->pluck('transactionContext', $options);
+
+ $res = $this->connection->executeSql([
+ 'sql' => $sql,
+ 'session' => $session->name()
+ ] + $options);
+
+ return $this->createResult($session, $res, $context);
+ }
+
+ /**
+ * Lookup rows in a database.
+ *
+ * @param Session $session The session in which to read data.
+ * @param string $table The table name.
+ * @param KeySet $keySet The KeySet to select rows.
+ * @param array $columns A list of column names to return.
+ * @param array $options [optional] {
+ * Configuration Options.
+ *
+ * @type string $index The name of an index on the table.
+ * @type int $offset The number of rows to offset results by.
+ * @type int $limit The number of results to return.
+ * }
+ * @return Result
+ */
+ public function read(Session $session, $table, KeySet $keySet, array $columns, array $options = [])
+ {
+ $options += [
+ 'index' => null,
+ 'limit' => null,
+ 'offset' => null,
+ 'transactionContext' => null
+ ];
+
+ $context = $this->pluck('transactionContext', $options);
+ $res = $this->connection->read([
+ 'table' => $table,
+ 'session' => $session->name(),
+ 'columns' => $columns,
+ 'keySet' => $this->flattenKeySet($keySet)
+ ] + $options);
+
+ return $this->createResult($session, $res, $context);
+ }
+
+ /**
+ * Create a read/write transaction.
+ *
+ * @todo if a transaction is already available on the session, get it instead
+ * of starting a new one?
+ *
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest
+ *
+ * @param Session $session The session to start the transaction in.
+ * @param array $options [optional] Configuration options.
+ * @return Transaction
+ */
+ public function transaction(Session $session, array $options = [])
+ {
+ $res = $this->beginTransaction($session, $options);
+ return $this->createTransaction($session, $res);
+ }
+
+ /**
+ * Create a read-only snapshot transaction.
+ *
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest
+ *
+ * @param Session $session The session to start the snapshot in.
+ * @param array $options [optional] Configuration options.
+ * @return Snapshot
+ */
+ public function snapshot(Session $session, array $options = [])
+ {
+ $res = $this->beginTransaction($session, $options);
+
+ return $this->createSnapshot($session, $res);
+ }
+
+ /**
+ * Execute a service call to begin a transaction or snapshot.
+ *
+ * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest
+ *
+ * @param Session $session The session to start the snapshot in.
+ * @param array $options [optional] Configuration options.
+ * @return array
+ */
+ private function beginTransaction(Session $session, array $options = [])
+ {
+ $options += [
+ 'transactionOptions' => []
+ ];
+
+ return $this->connection->beginTransaction($options + [
+ 'session' => $session->name(),
+ ]);
+ }
+
+ /**
+ * Create a Transaction instance from a response object.
+ *
+ * @param Session $session The session the transaction belongs to.
+ * @param array $res The transaction response.
+ * @return Transaction
+ */
+ private function createTransaction(Session $session, array $res)
+ {
+ return new Transaction($this, $session, $res['id']);
+ }
+
+ /**
+ * Create a Snapshot instance from a response object.
+ *
+ * @param Session $session The session the snapshot belongs to.
+ * @param array $res The snapshot response.
+ * @return Snapshot
+ */
+ private function createSnapshot(Session $session, array $res)
+ {
+ $timestamp = null;
+ if (isset($res['readTimestamp'])) {
+ $timestamp = $this->mapper->createTimestampWithNanos($res['readTimestamp']);
+ }
+
+ return new Snapshot($this, $session, $res['id'], $timestamp);
+ }
+
+ /**
+ * Transform a service read or executeSql response to a friendly result.
+ *
+ * @codingStandardsIgnoreStart
+ * @param Session $session The current session.
+ * @param array $res [ResultSet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet)
+ * @param string $transactionContext
+ * @return Result
+ * @codingStandardsIgnoreEnd
+ */
+ private function createResult(Session $session, array $res, $transactionContext)
+ {
+ $columns = isset($res['metadata']['rowType']['fields'])
+ ? $res['metadata']['rowType']['fields']
+ : [];
+
+ $rows = [];
+ if (isset($res['rows'])) {
+ foreach ($res['rows'] as $row) {
+ $rows[] = $this->mapper->decodeValues($columns, $row);
+ }
+ }
+
+ $options = [];
+ if (isset($res['metadata']['transaction']['id'])) {
+ if ($transactionContext === SessionPoolInterface::CONTEXT_READ) {
+ $options['snapshot'] = $this->createSnapshot($session, $res['metadata']['transaction']);
+ } else {
+ $options['transaction'] = $this->createTransaction($session, $res['metadata']['transaction']);
+ }
+ }
+
+ return new Result($res, $rows, $options);
+ }
+
+ /**
+ * Convert a KeySet object to an API-ready array.
+ *
+ * @param KeySet $keySet The keySet object.
+ * @return array [KeySet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#keyset)
+ */
+ private function flattenKeySet(KeySet $keySet)
+ {
+ $keyRanges = $keySet->ranges();
+ if ($keyRanges) {
+ $ranges = [];
+ foreach ($keyRanges as $range) {
+ $types = $range->types();
+
+ $start = $range->start();
+ $range->setStart($types['start'], $this->mapper->encodeValuesAsSimpleType($start));
+
+ $end = $range->end();
+ $range->setEnd($types['end'], $this->mapper->encodeValuesAsSimpleType($end));
+
+ $ranges[] = $range;
+ }
+
+ $keySet->setRanges($ranges);
+ }
+
+ $keys = $keySet->keySetObject();
+ if (!empty($keys['keys'])) {
+ $keys['keys'] = $this->mapper->encodeValuesAsSimpleType($keys['keys']);
+ }
+
+ return $this->arrayFilterRemoveNull($keys);
+ }
+
+ /**
+ * Represent the class in a more readable and digestable fashion.
+ *
+ * @access private
+ * @codeCoverageIgnore
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'connection' => get_class($this->connection),
+ ];
+ }
+}
diff --git a/src/Spanner/README.md b/src/Spanner/README.md
new file mode 100644
index 000000000000..de583242a7c3
--- /dev/null
+++ b/src/Spanner/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP Spanner
+
+> Idiomatic PHP client for [Cloud Spanner](https://cloud.google.com/spanner/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-spanner/latest/spanner/spannerclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-spanner
+```
diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php
new file mode 100644
index 000000000000..c168438ce963
--- /dev/null
+++ b/src/Spanner/Result.php
@@ -0,0 +1,203 @@
+spanner();
+ * $database = $spanner->connect('my-instance', 'my-database');
+ *
+ * $result = $database->execute('SELECT * FROM Posts');
+ * ```
+ *
+ * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet ResultSet
+ */
+class Result implements \IteratorAggregate
+{
+ /**
+ * @var array
+ */
+ private $result;
+
+ /**
+ * @var array
+ */
+ private $rows;
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * @param array $result The query or read result.
+ * @param array $rows The rows, formatted and decoded.
+ * @param array $options Additional result options and info.
+ */
+ public function __construct(array $result, array $rows, array $options = [])
+ {
+ $this->result = $result;
+ $this->rows = $rows;
+ $this->options = $options;
+ }
+
+ /**
+ * Return result metadata
+ *
+ * Example:
+ * ```
+ * $metadata = $result->metadata();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @return array [ResultSetMetadata](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetMetadata).
+ * @codingStandardsIgnoreEnd
+ */
+ public function metadata()
+ {
+ return $this->result['metadata'];
+ }
+
+ /**
+ * Return the formatted and decoded rows.
+ *
+ * Example:
+ * ```
+ * $rows = $result->rows();
+ * ```
+ *
+ * @return array|null
+ */
+ public function rows()
+ {
+ return $this->rows;
+ }
+
+ /**
+ * Return the first row, or null.
+ *
+ * Useful when selecting a single row.
+ *
+ * Example:
+ * ```
+ * $row = $result->firstRow();
+ * ```
+ *
+ * @return array|null
+ */
+ public function firstRow()
+ {
+ return (isset($this->rows[0]))
+ ? $this->rows[0]
+ : null;
+ }
+
+ /**
+ * Get the query plan and execution statistics for the query that produced
+ * this result set.
+ *
+ * Stats are not returned by default.
+ *
+ * Example:
+ * ```
+ * $stats = $result->stats();
+ * ```
+ *
+ * ```
+ * // Executing a query with stats returned.
+ * $res = $database->execute('SELECT * FROM Posts', [
+ * 'queryMode' => 'PROFILE'
+ * ]);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @return array|null [ResultSetStats](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetStats).
+ * @codingStandardsIgnoreEnd
+ */
+ public function stats()
+ {
+ return (isset($this->result['stats']))
+ ? $this->result['stats']
+ : null;
+ }
+
+ /**
+ * Returns a transaction which was begun in the read or execute, if one exists.
+ *
+ * Example:
+ * ```
+ * $transaction = $result->transaction();
+ * ```
+ *
+ * @return Transaction|null
+ */
+ public function transaction()
+ {
+ return (isset($this->options['transaction']))
+ ? $this->options['transaction']
+ : null;
+ }
+
+ /**
+ * Returns a snapshot which was begun in the read or execute, if one exists.
+ *
+ * Example:
+ * ```
+ * $snapshot = $result->snapshot();
+ * ```
+ *
+ * @return Snapshot|null
+ */
+ public function snapshot()
+ {
+ return (isset($this->options['snapshot']))
+ ? $this->options['snapshot']
+ : null;
+ }
+
+ /**
+ * Get the entire query or read response as given by the API.
+ *
+ * Example:
+ * ```
+ * $info = $result->info();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @return array [ResultSet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet).
+ * @codingStandardsIgnoreEnd
+ */
+ public function info()
+ {
+ return $this->result;
+ }
+
+ /**
+ * @access private
+ */
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->rows);
+ }
+}
diff --git a/src/Spanner/Session/Session.php b/src/Spanner/Session/Session.php
new file mode 100644
index 000000000000..c31265ee87cc
--- /dev/null
+++ b/src/Spanner/Session/Session.php
@@ -0,0 +1,170 @@
+spanner();
+ *
+ * $sessionClient = $spanner->sessionClient();
+ * $session = $sessionClient->create('test-instance', 'test-database');
+ * ```
+ */
+class Session
+{
+ /**
+ * @var ConnectionInterface
+ */
+ private $connection;
+
+ /**
+ * @var string
+ */
+ private $projectId;
+
+ /**
+ * @var string
+ */
+ private $instance;
+
+ /**
+ * @var string
+ */
+ private $database;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @param ConnectionInterface $connection A connection to Cloud Spanner.
+ * @param string $projectId The project ID.
+ * @param string $instance The instance name.
+ * @param string $database The database name.
+ * @param string $name The session name.
+ */
+ public function __construct(ConnectionInterface $connection, $projectId, $instance, $database, $name)
+ {
+ $this->connection = $connection;
+ $this->projectId = $projectId;
+ $this->instance = $instance;
+ $this->database = $database;
+ $this->name = $name;
+ }
+
+ /**
+ * Return info on the session
+ *
+ * Example:
+ * ```
+ * print_r($session->info());
+ * ```
+ *
+ * @return array An array containing the `projectId`, `instance`, `database` and session `name` keys.
+ */
+ public function info()
+ {
+ return [
+ 'projectId' => $this->projectId,
+ 'instance' => $this->instance,
+ 'database' => $this->database,
+ 'name' => $this->name
+ ];
+ }
+
+ /**
+ * Check if the session exists.
+ *
+ * Example:
+ * ```
+ * if ($session->exists()) {
+ * echo 'The session is valid!';
+ * }
+ * ```
+ *
+ * @param array $options [optional] Configuration options.
+ * @return array
+ */
+ public function exists(array $options = [])
+ {
+ try {
+ $this->connection->getSession($options + [
+ 'name' => $this->name()
+ ]);
+
+ return true;
+ } catch (NotFoundException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Delete the session.
+ *
+ * Example:
+ * ```
+ * $session->delete();
+ * ```
+ *
+ * @param array $options [optional] Configuration options.
+ * @return void
+ */
+ public function delete(array $options = [])
+ {
+ return $this->connection->deleteSession($options + [
+ 'name' => $this->name()
+ ]);
+ }
+
+ /**
+ * Format the constituent parts of a session name into a fully qualified session name.
+ *
+ * @return string
+ */
+ public function name()
+ {
+ return SpannerClient::formatSessionName(
+ $this->projectId,
+ $this->instance,
+ $this->database,
+ $this->name
+ );
+ }
+
+ public function __debugInfo()
+ {
+ return [
+ 'connection' => get_class($this->connection),
+ 'projectId' => $this->projectId,
+ 'instance' => $this->instance,
+ 'database' => $this->database,
+ 'name' => $this->name,
+ ];
+ }
+}
diff --git a/src/Spanner/Session/SessionClient.php b/src/Spanner/Session/SessionClient.php
new file mode 100644
index 000000000000..7024f198a5f8
--- /dev/null
+++ b/src/Spanner/Session/SessionClient.php
@@ -0,0 +1,109 @@
+spanner();
+ *
+ * $sessionClient = $spanner->sessionClient();
+ */
+class SessionClient
+{
+ /**
+ * @var ConnectionInterface
+ */
+ private $connection;
+
+ /**
+ * @var string
+ */
+ private $projectId;
+
+ /**
+ * Create a new Session Client.
+ *
+ * @param ConnectionInterface $connection A connection to the Cloud Spanner API
+ * @param string $projectId The current project ID
+ */
+ public function __construct(ConnectionInterface $connection, $projectId)
+ {
+ $this->connection = $connection;
+ $this->projectId = $projectId;
+ }
+
+ /**
+ * Create a new session in the given instance and database.
+ *
+ * Example:
+ * ```
+ * $sessionName = $sessionClient->create('test-instance', 'test-database');
+ * ```
+ *
+ * @param string $instance The simple instance name.
+ * @param string $database The simple database name.
+ * @param array $options [optional] Configuration options.
+ * @return Session|null If the operation succeeded, a Session object will be returned,
+ * otherwise null.
+ */
+ public function create($instance, $database, array $options = [])
+ {
+ $res = $this->connection->createSession($options + [
+ 'database' => SpannerClient::formatDatabaseName($this->projectId, $instance, $database)
+ ]);
+
+ $session = null;
+ if (isset($res['name'])) {
+ $session = $this->session($res['name']);
+ }
+
+ return $session;
+ }
+
+ public function session($sessionName)
+ {
+ return new Session(
+ $this->connection,
+ $this->projectId,
+ SpannerClient::parseInstanceFromSessionName($sessionName),
+ SpannerClient::parseDatabaseFromSessionName($sessionName),
+ SpannerClient::parseSessionFromSessionName($sessionName)
+ );
+ }
+
+ public function __debugInfo()
+ {
+ return [
+ 'connection' => get_class($this->connection),
+ 'projectId' => $this->projectId
+ ];
+ }
+}
diff --git a/src/Spanner/Session/SessionPool.php b/src/Spanner/Session/SessionPool.php
new file mode 100644
index 000000000000..40c859a64e7d
--- /dev/null
+++ b/src/Spanner/Session/SessionPool.php
@@ -0,0 +1,48 @@
+sessionClient = $sessionClient;
+ }
+
+ public function addSession(Session $session)
+ {
+ $this->sessions[] = $sessions;
+ }
+
+ public function session($instance, $database, $context, array $options = [])
+ {
+ return array_rand($this->sessions);
+ }
+
+ public function refreshSessions()
+ {
+ // send a request from each session to keep it alive.
+ }
+}
diff --git a/dev/src/SetStubConnectionTrait.php b/src/Spanner/Session/SessionPoolInterface.php
similarity index 73%
rename from dev/src/SetStubConnectionTrait.php
rename to src/Spanner/Session/SessionPoolInterface.php
index 3ffb3d5b606a..1294f2571d6a 100644
--- a/dev/src/SetStubConnectionTrait.php
+++ b/src/Spanner/Session/SessionPoolInterface.php
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Dev;
+namespace Google\Cloud\Spanner\Session;
-trait SetStubConnectionTrait
+interface SessionPoolInterface
{
- public function setConnection($conn)
- {
- $this->connection = $conn;
- }
+ const CONTEXT_READ = 'r';
+ const CONTEXT_READWRITE = 'rw';
+
+ public function session($instance, $database, $context, array $options = []);
}
diff --git a/src/Spanner/Session/SimpleSessionPool.php b/src/Spanner/Session/SimpleSessionPool.php
new file mode 100644
index 000000000000..2e058ca2dc2f
--- /dev/null
+++ b/src/Spanner/Session/SimpleSessionPool.php
@@ -0,0 +1,45 @@
+sessionClient = $sessionClient;
+ }
+
+ public function session($instance, $database, $mode, array $options = [])
+ {
+ if (!isset($this->sessions[$instance.$database.$mode])) {
+ $this->sessions[$instance.$database] = $this->sessionClient->create($instance, $database, $options);
+ }
+
+ return $this->sessions[$instance.$database];
+ }
+}
diff --git a/src/Spanner/Snapshot.php b/src/Spanner/Snapshot.php
new file mode 100644
index 000000000000..3a7d241e41e1
--- /dev/null
+++ b/src/Spanner/Snapshot.php
@@ -0,0 +1,85 @@
+spanner();
+ *
+ * $database = $spanner->connect('my-instance', 'my-database');
+ * $snapshot = $database->snapshot();
+ * ```
+ */
+class Snapshot
+{
+ use TransactionReadTrait;
+
+ /**
+ * @var Timestamp
+ */
+ private $readTimestamp;
+
+ /**
+ * @param Operation $operation The Operation instance.
+ * @param Session $session The session to use for spanner interactions.
+ * @param string $transactionId The Transaction ID.
+ * @param Timestamp $readTimestamp [optional] The read timestamp.
+ */
+ public function __construct(
+ Operation $operation,
+ Session $session,
+ $transactionId,
+ Timestamp $readTimestamp = null
+ ) {
+ $this->operation = $operation;
+ $this->session = $session;
+ $this->transactionId = $transactionId;
+ $this->readTimestamp = $readTimestamp;
+ $this->context = SessionPoolInterface::CONTEXT_READWRITE;
+ }
+
+ /**
+ * Retrieve the Read Timestamp.
+ *
+ * For snapshot read-only transactions, the read timestamp chosen for the
+ * transaction.
+ *
+ * Example:
+ * ```
+ * $timestamp = $transaction->readTimestamp();
+ * ```
+ *
+ * @return Timestamp
+ */
+ public function readTimestamp()
+ {
+ return $this->readTimestamp;
+ }
+}
diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php
new file mode 100644
index 000000000000..c010e5f9fa23
--- /dev/null
+++ b/src/Spanner/SpannerClient.php
@@ -0,0 +1,528 @@
+spanner();
+ * ```
+ *
+ * ```
+ * // SpannerClient can be instantiated directly.
+ * use Google\Cloud\Spanner\SpannerClient;
+ *
+ * $spanner = new SpannerClient();
+ * ```
+ */
+class SpannerClient
+{
+ use ArrayTrait;
+ use ClientTrait;
+ use LROTrait;
+ use ValidateTrait;
+
+ const VERSION = 'master';
+
+ const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data';
+ const ADMIN_SCOPE = 'https://www.googleapis.com/auth/spanner.admin';
+ const DEFAULT_NODE_COUNT = 1;
+
+ /**
+ * @var ConnectionInterface
+ */
+ protected $connection;
+
+ /**
+ * @var LongRunningConnectionInterface
+ */
+ private $lroConnection;
+
+ /**
+ * @var SessionClient
+ */
+ protected $sessionClient;
+
+ /**
+ * @var SessionPool
+ */
+ protected $sessionPool;
+
+ /**
+ * @var bool
+ */
+ private $returnInt64AsObject;
+
+ /**
+ * Create a Spanner client.
+ *
+ * @param array $config [optional] {
+ * Configuration Options.
+ *
+ * @type string $projectId The project ID from the Google Developer's
+ * Console.
+ * @type callable $authHttpHandler A handler used to deliver Psr7
+ * requests specifically for authentication.
+ * @type callable $httpHandler A handler used to deliver Psr7 requests.
+ * @type string $keyFile The contents of the service account
+ * credentials .json file retrieved from the Google Developers
+ * Console.
+ * @type string $keyFilePath The full path to your service account
+ * credentials .json file retrieved from the Google Developers
+ * Console.
+ * @type int $retries Number of retries for a failed request.
+ * **Defaults to** `3`.
+ * @type array $scopes Scopes to be used for the request.
+ * @type bool $returnInt64AsObject If true, 64 bit integers will be
+ * returned as a {@see Google\Cloud\Int64} object for 32 bit
+ * platform compatibility. **Defaults to** false.
+ * }
+ * @throws Google\Cloud\Exception\GoogleException
+ */
+ public function __construct(array $config = [])
+ {
+ $config += [
+ 'scopes' => [
+ self::FULL_CONTROL_SCOPE,
+ self::ADMIN_SCOPE
+ ],
+ 'returnInt64AsObject' => false
+ ];
+
+ $this->connection = new Grpc($this->configureAuthentication($config));
+ $this->lroConnection = new LongRunningConnection($this->connection);
+
+ $this->sessionClient = new SessionClient($this->connection, $this->projectId);
+ $this->sessionPool = new SimpleSessionPool($this->sessionClient);
+
+ $this->returnInt64AsObject = $config['returnInt64AsObject'];
+
+ $this->lroCallables = [
+ [
+ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata',
+ 'callable' => function ($instance) {
+ $name = InstanceAdminClient::parseInstanceFromInstanceName($instance['name']);
+ return $this->instance($name, $instance);
+ }
+ ], [
+ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata',
+ 'callable' => function ($database) {
+ $instanceName = DatabaseAdminClient::parseInstanceFromDatabaseName($database['name']);
+ $databaseName = DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']);
+
+ $instance = $this->instance($instanceName);
+ return $instance->database($databaseName);
+ }
+ ], [
+ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata',
+ 'callable' => function ($instance) {
+ $name = InstanceAdminClient::parseInstanceFromInstanceName($instance['name']);
+ return $this->instance($name, $instance);
+ }
+ ]
+ ];
+ }
+
+ /**
+ * List all available configurations.
+ *
+ * Example:
+ * ```
+ * $configurations = $spanner->configurations();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.ListInstanceConfigsRequest ListInstanceConfigsRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] {
+ * Configuration Options.
+ *
+ * @type int $pageSize Maximum number of results to return per
+ * request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
+ * }
+ * @return ItemIterator
+ */
+ public function configurations(array $options = [])
+ {
+ $resultLimit = $this->pluck('resultLimit', $options, false) ?: 0;
+
+ return new ItemIterator(
+ new PageIterator(
+ function (array $config) {
+ $name = InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config['name']);
+ return $this->configuration($name, $config);
+ },
+ [$this->connection, 'listConfigs'],
+ ['projectId' => InstanceAdminClient::formatProjectName($this->projectId)] + $options,
+ [
+ 'itemsKey' => 'instanceConfigs',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get a configuration by its name.
+ *
+ * NOTE: This method does not execute a service request and does not verify
+ * the existence of the given configuration. Unless you know with certainty
+ * that the configuration exists, it is advised that you use
+ * {@see Google\Cloud\Spanner\Configuration::exists()} to verify existence
+ * before attempting to use the configuration.
+ *
+ * Example:
+ * ```
+ * $configuration = $spanner->configuration($configurationName);
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#getinstanceconfigrequest GetInstanceConfigRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string $name The Configuration name.
+ * @param array $config [optional] The configuration details.
+ * @return Configuration
+ */
+ public function configuration($name, array $config = [])
+ {
+ return new Configuration($this->connection, $this->projectId, $name, $config);
+ }
+
+ /**
+ * Create a new instance.
+ *
+ * Example:
+ * ```
+ * $operation = $spanner->createInstance($configuration, 'my-instance');
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#createinstancerequest CreateInstanceRequest
+ *
+ * @param Configuration $config The configuration to use
+ * @param string $name The instance name
+ * @param array $options [optional] {
+ * Configuration options
+ *
+ * @type string $displayName **Defaults to** the value of $name.
+ * @type int $nodeCount **Defaults to** `1`.
+ * @type array $labels For more information, see
+ * [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html).
+ * }
+ * @return LongRunningOperation
+ * @codingStandardsIgnoreEnd
+ */
+ public function createInstance(Configuration $config, $name, array $options = [])
+ {
+ $options += [
+ 'displayName' => $name,
+ 'nodeCount' => self::DEFAULT_NODE_COUNT,
+ 'labels' => [],
+ 'operationName' => null,
+ ];
+
+ // This must always be set to CREATING, so overwrite anything else.
+ $options['state'] = State::CREATING;
+
+ $operation = $this->connection->createInstance([
+ 'instanceId' => $name,
+ 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name),
+ 'projectId' => InstanceAdminClient::formatProjectName($this->projectId),
+ 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name())
+ ] + $options);
+
+ return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables);
+ }
+
+ /**
+ * Lazily instantiate an instance.
+ *
+ * Example:
+ * ```
+ * $instance = $spanner->instance('my-instance');
+ * ```
+ *
+ * @param string $name The instance name
+ * @return Instance
+ */
+ public function instance($name, array $instance = [])
+ {
+ return new Instance(
+ $this->connection,
+ $this->sessionPool,
+ $this->lroConnection,
+ $this->lroCallables,
+ $this->projectId,
+ $name,
+ $this->returnInt64AsObject,
+ $instance
+ );
+ }
+
+ /**
+ * List instances in the project
+ *
+ * Example:
+ * ```
+ * $instances = $spanner->instances();
+ * ```
+ *
+ * @todo implement pagination!
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#listinstancesrequest ListInstancesRequest
+ * @codingStandardsIgnoreEnd
+ *
+ * @param array $options [optional] {
+ * Configuration options
+ *
+ * @type string $filter An expression for filtering the results of the
+ * request.
+ * @type int $pageSize Maximum number of results to return per
+ * request.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
+ * }
+ * @return ItemIterator
+ */
+ public function instances(array $options = [])
+ {
+ $options += [
+ 'filter' => null
+ ];
+
+ $resultLimit = $this->pluck('resultLimit', $options, false);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $instance) {
+ $name = InstanceAdminClient::parseInstanceFromInstanceName($instance['name']);
+ return $this->instance($name, $instance);
+ },
+ [$this->connection, 'listInstances'],
+ ['projectId' => InstanceAdminClient::formatProjectName($this->projectId)] + $options,
+ [
+ 'itemsKey' => 'instances',
+ 'resultLimit' => $resultLimit
+ ]
+ )
+ );
+ }
+
+ /**
+ * Connect to a database to run queries or mutations.
+ *
+ * Example:
+ * ```
+ * $database = $spanner->connect('my-instance', 'my-application-database');
+ * ```
+ *
+ * @param Instance|string $instance The instance object or instance name.
+ * @param string $name The database name.
+ * @return Database
+ */
+ public function connect($instance, $name)
+ {
+ if (is_string($instance)) {
+ $instance = $this->instance($instance);
+ }
+
+ $database = $instance->database($name);
+
+ return $database;
+ }
+
+ /**
+ * Create a new KeySet object
+ *
+ * @param array $options [optional] {
+ * Configuration Options
+ *
+ * @type array $keys A list of keys
+ * @type KeyRange[] $ranges A list of key ranges
+ * @type bool $all Whether to include all keys in a table
+ * }
+ * @return KeySet
+ */
+ public function keySet(array $options = [])
+ {
+ return new KeySet($options);
+ }
+
+ /**
+ * Create a new KeyRange object
+ *
+ * @param array $options [optional] {
+ * Configuration Options.
+ *
+ * @type string $startType Either "open" or "closed". Use constants
+ * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for
+ * guaranteed correctness.
+ * @type array $start The key with which to start the range.
+ * @type string $endType Either "open" or "closed". Use constants
+ * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for
+ * guaranteed correctness.
+ * @type array $end The key with which to end the range.
+ * }
+ * @return KeyRange
+ */
+ public function keyRange(array $options = [])
+ {
+ return new KeyRange($options);
+ }
+
+ /**
+ * Create a Bytes object.
+ *
+ * Example:
+ * ```
+ * $bytes = $spanner->bytes('hello world');
+ * ```
+ *
+ * @param string|resource|StreamInterface $value The bytes value.
+ * @return Bytes
+ */
+ public function bytes($bytes)
+ {
+ return new Bytes($bytes);
+ }
+
+ /**
+ * Create a Date object.
+ *
+ * Example:
+ * ```
+ * $date = $spanner->date(new \DateTime('1995-02-04'));
+ * ```
+ *
+ * @param \DateTimeInterface $value The date value.
+ * @return Date
+ */
+ public function date(\DateTimeInterface $date)
+ {
+ return new Date($date);
+ }
+
+ /**
+ * Create a Timestamp object.
+ *
+ * Example:
+ * ```
+ * $timestamp = $spanner->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z'));
+ * ```
+ *
+ * @param \DateTimeInterface $value The timestamp value.
+ * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp.
+ * @return Timestamp
+ */
+ public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null)
+ {
+ return new Timestamp($timestamp, $nanoSeconds);
+ }
+
+ /**
+ * Create an Int64 object. This can be used to work with 64 bit integers as
+ * a string value while on a 32 bit platform.
+ *
+ * Example:
+ * ```
+ * $int64 = $spanner->int64('9223372036854775807');
+ * ```
+ *
+ * @param string $value
+ * @return Int64
+ */
+ public function int64($value)
+ {
+ return new Int64($value);
+ }
+
+ /**
+ * Create a Duration object.
+ *
+ * Example:
+ * ```
+ * $duration = $spanner->duration(100, 00001);
+ * ```
+ *
+ * @param int $seconds The number of seconds in the duration.
+ * @param int $nanos [optional] The number of nanoseconds in the duration.
+ * **Defaults to** `0`.
+ * @return Duration
+ */
+ public function duration($seconds, $nanos = 0)
+ {
+ return new Duration($seconds, $nanos);
+ }
+
+ /**
+ * Get the session client
+ *
+ * Example:
+ * ```
+ * $sessionClient = $spanner->sessionClient();
+ * ```
+ *
+ * @return SessionClient
+ */
+ public function sessionClient()
+ {
+ return $this->sessionClient;
+ }
+
+ /**
+ * Resume a Long Running Operation
+ *
+ * @param string $operationName The Long Running Operation name.
+ * @return LongRunningOperation
+ */
+ public function resumeOperation($operationName)
+ {
+ return $this->lro($this->lroConnection, $operationName, $this->lroCallables);
+ }
+}
diff --git a/src/Spanner/Timestamp.php b/src/Spanner/Timestamp.php
new file mode 100644
index 000000000000..8968315d9d0e
--- /dev/null
+++ b/src/Spanner/Timestamp.php
@@ -0,0 +1,100 @@
+timestamp(new \DateTime('2003-02-05 11:15:02.421827Z'));
+ * ```
+ */
+class Timestamp implements ValueInterface
+{
+ const FORMAT = 'Y-m-d\TH:i:s.u\Z';
+ const FORMAT_INTERPOLATE = 'Y-m-d\TH:i:s.%\s\Z';
+
+ /**
+ * @var \DateTimeInterface
+ */
+ private $value;
+
+ /**
+ * @var int
+ */
+ private $nanoSeconds;
+
+ /**
+ * @param \DateTimeInterface $value The timestamp value.
+ * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp.
+ */
+ public function __construct(\DateTimeInterface $value, $nanoSeconds = null)
+ {
+ $this->value = $value;
+ $this->nanoSeconds = $nanoSeconds ?: (int) $this->value->format('u');
+ }
+
+ /**
+ * Get the underlying `\DateTimeInterface` implementation.
+ *
+ * @return \DateTimeInterface
+ */
+ public function get()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Get the type.
+ *
+ * @return string
+ */
+ public function type()
+ {
+ return ValueMapper::TYPE_TIMESTAMP;
+ }
+
+ /**
+ * Format the value as a string.
+ *
+ * @return string
+ */
+ public function formatAsString()
+ {
+ $this->value->setTimezone(new \DateTimeZone('UTC'));
+ $ns = str_pad((string) $this->nanoSeconds, 6, '0', STR_PAD_LEFT);
+ return sprintf($this->value->format(self::FORMAT_INTERPOLATE), $ns);
+ }
+
+ /**
+ * Format the value as a string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->formatAsString();
+ }
+}
diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php
new file mode 100644
index 000000000000..d3bfa0b0e724
--- /dev/null
+++ b/src/Spanner/Transaction.php
@@ -0,0 +1,403 @@
+spanner();
+ *
+ * $database = $spanner->connect('my-instance', 'my-database');
+ *
+ * $database->runTransaction(function (Transaction $t) {
+ * // do stuff.
+ *
+ * $t->commit();
+ * });
+ * ```
+ *
+ * ```
+ * // Get a transaction to manage manually.
+ * $transaction = $database->transaction();
+ * ```
+ */
+class Transaction
+{
+ use TransactionReadTrait;
+
+ const STATE_ACTIVE = 0;
+ const STATE_ROLLED_BACK = 1;
+ const STATE_COMMITTED = 2;
+
+ /**
+ * @var array
+ */
+ private $mutations = [];
+
+ /**
+ * @var int
+ */
+ private $state = self::STATE_ACTIVE;
+
+ /**
+ * @param Operation $operation The Operation instance.
+ * @param Session $session The session to use for spanner interactions.
+ * @param string $transactionId The Transaction ID.
+ */
+ public function __construct(
+ Operation $operation,
+ Session $session,
+ $transactionId
+ ) {
+ $this->operation = $operation;
+ $this->session = $session;
+ $this->transactionId = $transactionId;
+ $this->context = SessionPoolInterface::CONTEXT_READWRITE;
+ }
+
+ /**
+ * Enqueue an insert mutation.
+ *
+ * Example:
+ * ```
+ * $transaction->insert('Posts', [
+ * 'ID' => 10,
+ * 'title' => 'My New Post',
+ * 'content' => 'Hello World'
+ * ]);
+ * ```
+ *
+ * @param string $table The table to insert into.
+ * @param array $data The data to insert.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function insert($table, array $data)
+ {
+ return $this->insertBatch($table, [$data]);
+ }
+
+ /**
+ * Enqueue one or more insert mutations.
+ *
+ * Example:
+ * ```
+ * $transaction->insertBatch('Posts', [
+ * [
+ * 'ID' => 10,
+ * 'title' => 'My New Post',
+ * 'content' => 'Hello World'
+ * ]
+ * ]);
+ * ```
+ *
+ * @param string $table The table to insert into.
+ * @param array $dataSet The data to insert.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function insertBatch($table, array $dataSet)
+ {
+ $this->enqueue(Operation::OP_INSERT, $table, $dataSet);
+
+ return $this;
+ }
+
+ /**
+ * Enqueue an update mutation.
+ *
+ * Example:
+ * ```
+ * $transaction->update('Posts', [
+ * 'ID' => 10,
+ * 'title' => 'My New Post [Updated!]',
+ * 'content' => 'Modified Content'
+ * ]);
+ * ```
+ *
+ * @param string $table The table to update.
+ * @param array $data The data to update.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function update($table, array $data)
+ {
+ return $this->updateBatch($table, [$data]);
+ }
+
+ /**
+ * Enqueue one or more update mutations.
+ *
+ * Example:
+ * ```
+ * $transaction->updateBatch('Posts', [
+ * [
+ * 'ID' => 10,
+ * 'title' => 'My New Post [Updated!]',
+ * 'content' => 'Modified Content'
+ * ]
+ * ]);
+ * ```
+ *
+ * @param string $table The table to update.
+ * @param array $dataSet The data to update.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function updateBatch($table, array $dataSet)
+ {
+ $this->enqueue(Operation::OP_UPDATE, $table, $dataSet);
+
+ return $this;
+ }
+
+ /**
+ * Enqueue an insert or update mutation.
+ *
+ * Example:
+ * ```
+ * $transaction->insertOrUpdate('Posts', [
+ * 'ID' => 10,
+ * 'title' => 'My New Post',
+ * 'content' => 'Hello World'
+ * ]);
+ * ```
+ *
+ * @param string $table The table to insert into or update.
+ * @param array $data The data to insert or update.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function insertOrUpdate($table, array $data)
+ {
+ return $this->insertOrUpdateBatch($table, [$data]);
+ }
+
+ /**
+ * Enqueue one or more insert or update mutations.
+ *
+ * Example:
+ * ```
+ * $transaction->insertOrUpdateBatch('Posts', [
+ * [
+ * 'ID' => 10,
+ * 'title' => 'My New Post',
+ * 'content' => 'Hello World'
+ * ]
+ * ]);
+ * ```
+ *
+ * @param string $table The table to insert into or update.
+ * @param array $dataSet The data to insert or update.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function insertOrUpdateBatch($table, array $dataSet)
+ {
+ $this->enqueue(Operation::OP_INSERT_OR_UPDATE, $table, $dataSet);
+
+ return $this;
+ }
+
+ /**
+ * Enqueue an replace mutation.
+ *
+ * Example:
+ * ```
+ * $transaction->replace('Posts', [
+ * 'ID' => 10,
+ * 'title' => 'My New Post [Replaced]',
+ * 'content' => 'Hello Moon'
+ * ]);
+ * ```
+ *
+ * @param string $table The table to replace into.
+ * @param array $data The data to replace.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function replace($table, array $data)
+ {
+ return $this->replaceBatch($table, [$data]);
+ }
+
+ /**
+ * Enqueue one or more replace mutations.
+ *
+ * Example:
+ * ```
+ * $transaction->replaceBatch('Posts', [
+ * [
+ * 'ID' => 10,
+ * 'title' => 'My New Post [Replaced]',
+ * 'content' => 'Hello Moon'
+ * ]
+ * ]);
+ * ```
+ *
+ * @param string $table The table to replace into.
+ * @param array $dataSet The data to replace.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function replaceBatch($table, array $dataSet)
+ {
+ $this->enqueue(Operation::OP_REPLACE, $table, $dataSet);
+
+ return $this;
+ }
+
+ /**
+ * Enqueue an delete mutation.
+ *
+ * Example:
+ * ```
+ * $keySet = $spanner->keySet([
+ * 'keys' => [10]
+ * ]);
+ *
+ * $transaction->delete('Posts', $keySet);
+ * ```
+ *
+ * @param string $table The table to mutate.
+ * @param KeySet $keySet The KeySet to identify rows to delete.
+ * @return Transaction The transaction, to enable method chaining.
+ */
+ public function delete($table, KeySet $keySet)
+ {
+ $this->enqueue(Operation::OP_DELETE, $table, [$keySet]);
+
+ return $this;
+ }
+
+ /**
+ * Roll back a transaction.
+ *
+ * Rolls back a transaction, releasing any locks it holds. It is a good idea
+ * to call this for any transaction that includes one or more Read or
+ * ExecuteSql requests and ultimately decides not to commit.
+ *
+ * This closes the transaction, preventing any future API calls inside it.
+ *
+ * Rollback will NOT error if the transaction is not found or was already aborted.
+ *
+ * Example:
+ * ```
+ * $transaction->rollback();
+ * ```
+ *
+ * @param array $options [optional] Configuration Options.
+ * @return void
+ */
+ public function rollback(array $options = [])
+ {
+ if ($this->state !== self::STATE_ACTIVE) {
+ throw new \RuntimeException('The transaction cannot be rolled back because it is not active');
+ }
+
+ $this->state = self::STATE_ROLLED_BACK;
+
+ return $this->operation->rollback($this->session, $this->transactionId, $options);
+ }
+
+ /**
+ * Commit and end the transaction.
+ *
+ * It is advised that transactions be run inside
+ * {@see Google\Cloud\Spanner\Database::runTransaction()} in order to take
+ * advantage of automated transaction retry in case of a transaction aborted
+ * error.
+ *
+ * Example:
+ * ```
+ * $transaction->commit();
+ * ```
+ *
+ * @param array $options [optional] Configuration Options.
+ * @return Timestamp The commit timestamp.
+ * @throws \RuntimeException If the transaction is not active
+ * @throws \AbortedException If the commit is aborted for any reason.
+ */
+ public function commit(array $options = [])
+ {
+ if ($this->state !== self::STATE_ACTIVE) {
+ throw new \RuntimeException('The transaction cannot be committed because it is not active');
+ }
+
+ $this->state = self::STATE_COMMITTED;
+
+ $options['transactionId'] = $this->transactionId;
+ return $this->operation->commit($this->session, $this->mutations, $options);
+ }
+
+ /**
+ * Retrieve the Transaction State.
+ *
+ * Will be one of `Transaction::STATE_ACTIVE`,
+ * `Transaction::STATE_COMMITTED`, or `Transaction::STATE_ROLLED_BACK`.
+ *
+ * Example:
+ * ```
+ * $state = $transaction->state();
+ * ```
+ *
+ * @return int
+ */
+ public function state()
+ {
+ return $this->state;
+ }
+
+ /**
+ * Format, validate and enqueue mutations in the transaction.
+ *
+ * @param string $op The operation type.
+ * @param string $table The table name
+ * @param array $dataSet the mutations to enqueue
+ * @return void
+ */
+ private function enqueue($op, $table, array $dataSet)
+ {
+ foreach ($dataSet as $data) {
+ if ($op === Operation::OP_DELETE) {
+ $this->mutations[] = $this->operation->deleteMutation($table, $data);
+ } else {
+ $this->mutations[] = $this->operation->mutation($op, $table, $data);
+ }
+ }
+ }
+}
diff --git a/src/Spanner/TransactionConfigurationTrait.php b/src/Spanner/TransactionConfigurationTrait.php
new file mode 100644
index 000000000000..81d6704c226a
--- /dev/null
+++ b/src/Spanner/TransactionConfigurationTrait.php
@@ -0,0 +1,152 @@
+ false,
+ 'transactionType' => SessionPoolInterface::CONTEXT_READ,
+ 'transactionId' => null
+ ];
+
+ $type = null;
+
+ $context = $this->pluck('transactionType', $options);
+ $id = $this->pluck('transactionId', $options);
+ if (!is_null($id)) {
+ $type = 'id';
+ $transactionOptions = $id;
+ } elseif ($context === SessionPoolInterface::CONTEXT_READ) {
+ $transactionOptions = $this->configureSnapshotOptions($options);
+ } elseif ($context === SessionPoolInterface::CONTEXT_READWRITE) {
+ $transactionOptions = $this->configureTransactionOptions();
+ } else {
+ throw new \BadMethodCallException(sprintf(
+ 'Invalid transaction context %s',
+ $context
+ ));
+ }
+
+ $begin = $this->pluck('begin', $options);
+ if (is_null($type)) {
+ $type = ($begin) ? 'begin' : 'singleUse';
+ }
+
+ return [
+ [$type => $transactionOptions],
+ $context
+ ];
+ }
+
+ private function configureTransactionOptions()
+ {
+ return [
+ 'readWrite' => []
+ ];
+ }
+
+ /**
+ * Create a Read Only single use transaction.
+ *
+ * @param array $options Configuration Options.
+ * @return array
+ */
+ private function configureSnapshotOptions(array &$options)
+ {
+ $options += [
+ 'returnReadTimestamp' => null,
+ 'strong' => null,
+ 'readTimestamp' => null,
+ 'exactStaleness' => null,
+ 'minReadTimestamp' => null,
+ 'maxStaleness' => null,
+ ];
+
+ $transactionOptions = [
+ 'readOnly' => $this->arrayFilterRemoveNull([
+ 'returnReadTimestamp' => $this->pluck('returnReadTimestamp', $options),
+ 'strong' => $this->pluck('strong', $options),
+ 'minReadTimestamp' => $this->pluck('minReadTimestamp', $options),
+ 'maxStaleness' => $this->pluck('maxStaleness', $options),
+ 'readTimestamp' => $this->pluck('readTimestamp', $options),
+ 'exactStaleness' => $this->pluck('exactStaleness', $options),
+ ])
+ ];
+
+ if (empty($transactionOptions['readOnly'])) {
+ $transactionOptions['readOnly']['strong'] = true;
+ }
+
+ $timestampFields = [
+ 'minReadTimestamp',
+ 'readTimestamp'
+ ];
+
+ $durationFields = [
+ 'exactStaleness',
+ 'maxStaleness'
+ ];
+
+ foreach ($timestampFields as $tsf) {
+ if (isset($transactionOptions['readOnly'][$tsf])) {
+ $field = $transactionOptions['readOnly'][$tsf];
+ if (!($field instanceof Timestamp)) {
+ throw new \BadMethodCallException(sprintf(
+ 'Read Only Transaction Configuration Field %s must be an instance of Timestamp',
+ $tsf
+ ));
+ }
+
+ $transactionOptions['readOnly'][$tsf] = $field->formatAsString();
+ }
+ }
+
+ foreach ($durationFields as $df) {
+ if (isset($transactionOptions['readOnly'][$df])) {
+ $field = $transactionOptions['readOnly'][$df];
+ if (!($field instanceof Duration)) {
+ throw new \BadMethodCallException(sprintf(
+ 'Read Only Transaction Configuration Field %s must be an instance of Duration',
+ $df
+ ));
+ }
+
+ $transactionOptions['readOnly'][$df] = $field->get();
+ }
+ }
+
+ return $transactionOptions;
+ }
+}
diff --git a/src/Spanner/TransactionReadTrait.php b/src/Spanner/TransactionReadTrait.php
new file mode 100644
index 000000000000..44ba7d397e7e
--- /dev/null
+++ b/src/Spanner/TransactionReadTrait.php
@@ -0,0 +1,138 @@
+execute(
+ * 'SELECT * FROM Users WHERE id = @userId',
+ * [
+ * 'parameters' => [
+ * 'userId' => 1
+ * ]
+ * ]
+ * );
+ * ```
+ * @param string $sql The query string to execute.
+ * @param array $options [optional] {
+ * Configuration options.
+ *
+ * @type array $parameters A key/value array of Query Parameters, where
+ * the key is represented in the query string prefixed by a `@`
+ * symbol.
+ * }
+ * @return Result
+ */
+ public function execute($sql, array $options = [])
+ {
+ $options['transactionType'] = $this->context;
+ $options['transactionId'] = $this->transactionId;
+
+ list($transactionOptions, $context) = $this->transactionSelector($options);
+ $options['transaction'] = $transactionOptions;
+ $options['transactionContext'] = $context;
+
+ return $this->operation->execute($this->session, $sql, $options);
+ }
+
+ /**
+ * Lookup rows in a table.
+ *
+ * Note that if no KeySet is specified, all rows in a table will be
+ * returned.
+ *
+ * Example:
+ * ```
+ * $keySet = $spanner->keySet([
+ * 'keys' => [10]
+ * ]);
+ *
+ * $result = $database->read('Posts', [
+ * 'keySet' => $keySet
+ * ]);
+ * ```
+ *
+ * @param string $table The table name.
+ * @param KeySet $keySet The KeySet to select rows.
+ * @param array $columns A list of column names to return.
+ * @param array $options [optional] {
+ * Configuration Options.
+ *
+ * @type string $index The name of an index on the table.
+ * @type int $offset The number of rows to offset results by.
+ * @type int $limit The number of results to return.
+ * }
+ * @return Result
+ */
+ public function read($table, KeySet $keySet, array $columns, array $options = [])
+ {
+ $options['transactionType'] = $this->context;
+ $options['transactionId'] = $this->transactionId;
+
+ list($transactionOptions, $context) = $this->transactionSelector($options);
+ $options['transaction'] = $transactionOptions;
+ $options['transactionContext'] = $context;
+
+ return $this->operation->read($this->session, $table, $keySet, $columns, $options);
+ }
+
+ /**
+ * Retrieve the Transaction ID.
+ *
+ * Example:
+ * ```
+ * $id = $transaction->id();
+ * ```
+ *
+ * @return string
+ */
+ public function id()
+ {
+ return $this->transactionId;
+ }
+}
diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php
new file mode 100644
index 000000000000..226893168443
--- /dev/null
+++ b/src/Spanner/V1/SpannerClient.php
@@ -0,0 +1,1180 @@
+createSession($formattedDatabase);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * Many parameters require resource names to be formatted in a particular way. To assist
+ * with these names, this class includes a format method for each type of name, and additionally
+ * a parse method to extract the individual identifiers contained within names that are
+ * returned.
+ */
+class SpannerClient
+{
+ /**
+ * The default address of the service.
+ */
+ const SERVICE_ADDRESS = 'wrenchworks.googleapis.com';
+
+ /**
+ * The default port of the service.
+ */
+ const DEFAULT_SERVICE_PORT = 443;
+
+ /**
+ * The default timeout for non-retrying methods.
+ */
+ const DEFAULT_TIMEOUT_MILLIS = 30000;
+
+ /**
+ * The name of the code generator, to be included in the agent header.
+ */
+ const CODEGEN_NAME = 'gapic';
+
+ /**
+ * The code generator version, to be included in the agent header.
+ */
+ const CODEGEN_VERSION = '0.1.0';
+
+ private static $databaseNameTemplate;
+ private static $sessionNameTemplate;
+
+ private $grpcCredentialsHelper;
+ private $spannerStub;
+ private $scopes;
+ private $defaultCallSettings;
+ private $descriptors;
+
+ /**
+ * Formats a string containing the fully-qualified path to represent
+ * a database resource.
+ */
+ public static function formatDatabaseName($project, $instance, $database)
+ {
+ return self::getDatabaseNameTemplate()->render([
+ 'project' => $project,
+ 'instance' => $instance,
+ 'database' => $database,
+ ]);
+ }
+
+ /**
+ * Formats a string containing the fully-qualified path to represent
+ * a session resource.
+ */
+ public static function formatSessionName($project, $instance, $database, $session)
+ {
+ return self::getSessionNameTemplate()->render([
+ 'project' => $project,
+ 'instance' => $instance,
+ 'database' => $database,
+ 'session' => $session,
+ ]);
+ }
+
+ /**
+ * Parses the project from the given fully-qualified path which
+ * represents a database resource.
+ */
+ public static function parseProjectFromDatabaseName($databaseName)
+ {
+ return self::getDatabaseNameTemplate()->match($databaseName)['project'];
+ }
+
+ /**
+ * Parses the instance from the given fully-qualified path which
+ * represents a database resource.
+ */
+ public static function parseInstanceFromDatabaseName($databaseName)
+ {
+ return self::getDatabaseNameTemplate()->match($databaseName)['instance'];
+ }
+
+ /**
+ * Parses the database from the given fully-qualified path which
+ * represents a database resource.
+ */
+ public static function parseDatabaseFromDatabaseName($databaseName)
+ {
+ return self::getDatabaseNameTemplate()->match($databaseName)['database'];
+ }
+
+ /**
+ * Parses the project from the given fully-qualified path which
+ * represents a session resource.
+ */
+ public static function parseProjectFromSessionName($sessionName)
+ {
+ return self::getSessionNameTemplate()->match($sessionName)['project'];
+ }
+
+ /**
+ * Parses the instance from the given fully-qualified path which
+ * represents a session resource.
+ */
+ public static function parseInstanceFromSessionName($sessionName)
+ {
+ return self::getSessionNameTemplate()->match($sessionName)['instance'];
+ }
+
+ /**
+ * Parses the database from the given fully-qualified path which
+ * represents a session resource.
+ */
+ public static function parseDatabaseFromSessionName($sessionName)
+ {
+ return self::getSessionNameTemplate()->match($sessionName)['database'];
+ }
+
+ /**
+ * Parses the session from the given fully-qualified path which
+ * represents a session resource.
+ */
+ public static function parseSessionFromSessionName($sessionName)
+ {
+ return self::getSessionNameTemplate()->match($sessionName)['session'];
+ }
+
+ private static function getDatabaseNameTemplate()
+ {
+ if (self::$databaseNameTemplate == null) {
+ self::$databaseNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}');
+ }
+
+ return self::$databaseNameTemplate;
+ }
+
+ private static function getSessionNameTemplate()
+ {
+ if (self::$sessionNameTemplate == null) {
+ self::$sessionNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}/sessions/{session}');
+ }
+
+ return self::$sessionNameTemplate;
+ }
+
+ private static function getGrpcStreamingDescriptors()
+ {
+ return [
+ 'executeStreamingSql' => [
+ 'grpcStreamingType' => 'ServerStreaming',
+ ],
+ 'streamingRead' => [
+ 'grpcStreamingType' => 'ServerStreaming',
+ ],
+ ];
+ }
+
+ // TODO(garrettjones): add channel (when supported in gRPC)
+ /**
+ * Constructor.
+ *
+ * @param array $options {
+ * Optional. Options for configuring the service API wrapper.
+ *
+ * @type string $serviceAddress The domain name of the API remote host.
+ * Default 'spanner.googleapis.com'.
+ * @type mixed $port The port on which to connect to the remote host. Default 443.
+ * @type \Grpc\ChannelCredentials $sslCreds
+ * A `ChannelCredentials` for use with an SSL-enabled channel.
+ * Default: a credentials object returned from
+ * \Grpc\ChannelCredentials::createSsl()
+ * @type array $scopes A string array of scopes to use when acquiring credentials.
+ * Default the scopes for the Google Cloud Spanner API.
+ * @type array $retryingOverride
+ * An associative array of string => RetryOptions, where the keys
+ * are method names (e.g. 'createFoo'), that overrides default retrying
+ * settings. A value of null indicates that the method in question should
+ * not retry.
+ * @type int $timeoutMillis The timeout in milliseconds to use for calls
+ * that don't use retries. For calls that use retries,
+ * set the timeout in RetryOptions.
+ * Default: 30000 (30 seconds)
+ * @type string $appName The codename of the calling service. Default 'gax'.
+ * @type string $appVersion The version of the calling service.
+ * Default: the current version of GAX.
+ * @type \Google\Auth\CredentialsLoader $credentialsLoader
+ * A CredentialsLoader object created using the
+ * Google\Auth library.
+ * }
+ */
+ public function __construct($options = [])
+ {
+ $defaultOptions = [
+ 'serviceAddress' => self::SERVICE_ADDRESS,
+ 'port' => self::DEFAULT_SERVICE_PORT,
+ 'scopes' => [
+ 'https://www.googleapis.com/auth/cloud-platform',
+ 'https://www.googleapis.com/auth/spanner.data',
+ ],
+ 'retryingOverride' => null,
+ 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
+ 'appName' => 'gax',
+ 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ ];
+ $options = array_merge($defaultOptions, $options);
+
+ $headerDescriptor = new AgentHeaderDescriptor([
+ 'clientName' => $options['appName'],
+ 'clientVersion' => $options['appVersion'],
+ 'codeGenName' => self::CODEGEN_NAME,
+ 'codeGenVersion' => self::CODEGEN_VERSION,
+ 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'phpVersion' => phpversion(),
+ ]);
+
+ $defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
+ $this->descriptors = [
+ 'createSession' => $defaultDescriptors,
+ 'getSession' => $defaultDescriptors,
+ 'deleteSession' => $defaultDescriptors,
+ 'executeSql' => $defaultDescriptors,
+ 'executeStreamingSql' => $defaultDescriptors,
+ 'read' => $defaultDescriptors,
+ 'streamingRead' => $defaultDescriptors,
+ 'beginTransaction' => $defaultDescriptors,
+ 'commit' => $defaultDescriptors,
+ 'rollback' => $defaultDescriptors,
+ ];
+ $grpcStreamingDescriptors = self::getGrpcStreamingDescriptors();
+ foreach ($grpcStreamingDescriptors as $method => $grpcStreamingDescriptor) {
+ $this->descriptors[$method]['grpcStreamingDescriptor'] = $grpcStreamingDescriptor;
+ }
+
+ $clientConfigJsonString = file_get_contents(__DIR__.'/resources/spanner_client_config.json');
+ $clientConfig = json_decode($clientConfigJsonString, true);
+ $this->defaultCallSettings =
+ CallSettings::load(
+ 'google.spanner.v1.Spanner',
+ $clientConfig,
+ $options['retryingOverride'],
+ GrpcConstants::getStatusCodeNames(),
+ $options['timeoutMillis']
+ );
+
+ $this->scopes = $options['scopes'];
+
+ $createStubOptions = [];
+ if (array_key_exists('sslCreds', $options)) {
+ $createStubOptions['sslCreds'] = $options['sslCreds'];
+ }
+ $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions);
+ $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions);
+
+ $createSpannerStubFunction = function ($hostname, $opts) {
+ return new SpannerGrpcClient($hostname, $opts);
+ };
+ if (array_key_exists('createSpannerStubFunction', $options)) {
+ $createSpannerStubFunction = $options['createSpannerStubFunction'];
+ }
+ $this->spannerStub = $this->grpcCredentialsHelper->createStub(
+ $createSpannerStubFunction,
+ $options['serviceAddress'],
+ $options['port'],
+ $createStubOptions
+ );
+ }
+
+ /**
+ * Creates a new session. A session can be used to perform
+ * transactions that read and/or modify data in a Cloud Spanner database.
+ * Sessions are meant to be reused for many consecutive
+ * transactions.
+ *
+ * Sessions can only execute one transaction at a time. To execute
+ * multiple concurrent read-write/write-only transactions, create
+ * multiple sessions. Note that standalone reads and queries use a
+ * transaction internally, and count toward the one transaction
+ * limit.
+ *
+ * Cloud Spanner limits the number of sessions that can exist at any given
+ * time; thus, it is a good idea to delete idle and/or unneeded sessions.
+ * Aside from explicit deletes, Cloud Spanner can delete sessions for
+ * which no operations are sent for more than an hour, or due to
+ * internal errors. If a session is deleted, requests to it
+ * return `NOT_FOUND`.
+ *
+ * Idle sessions can be kept alive by sending a trivial SQL query
+ * periodically, e.g., `"SELECT 1"`.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedDatabase = SpannerClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+ * $response = $spannerClient->createSession($formattedDatabase);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $database Required. The database in which the new session is created.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\v1\Session
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function createSession($database, $optionalArgs = [])
+ {
+ $request = new CreateSessionRequest();
+ $request->setDatabase($database);
+
+ $mergedSettings = $this->defaultCallSettings['createSession']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'CreateSession',
+ $mergedSettings,
+ $this->descriptors['createSession']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Gets a session. Returns `NOT_FOUND` if the session does not exist.
+ * This is mainly useful for determining whether a session is still
+ * alive.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $response = $spannerClient->getSession($formattedName);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $name Required. The name of the session to retrieve.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\v1\Session
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function getSession($name, $optionalArgs = [])
+ {
+ $request = new GetSessionRequest();
+ $request->setName($name);
+
+ $mergedSettings = $this->defaultCallSettings['getSession']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'GetSession',
+ $mergedSettings,
+ $this->descriptors['getSession']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Ends a session, releasing server resources associated with it.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $spannerClient->deleteSession($formattedName);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $name Required. The name of the session to delete.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function deleteSession($name, $optionalArgs = [])
+ {
+ $request = new DeleteSessionRequest();
+ $request->setName($name);
+
+ $mergedSettings = $this->defaultCallSettings['deleteSession']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'DeleteSession',
+ $mergedSettings,
+ $this->descriptors['deleteSession']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Executes an SQL query, returning all rows in a single reply. This
+ * method cannot be used to return a result set larger than 10 MiB;
+ * if the query yields more data than that, the query fails with
+ * a `FAILED_PRECONDITION` error.
+ *
+ * Queries inside read-write transactions might return `ABORTED`. If
+ * this occurs, the application should restart the transaction from
+ * the beginning. See [Transaction][google.spanner.v1.Transaction] for more details.
+ *
+ * Larger result sets can be fetched in streaming fashion by calling
+ * [ExecuteStreamingSql][google.spanner.v1.Spanner.ExecuteStreamingSql] instead.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $sql = "";
+ * $response = $spannerClient->executeSql($formattedSession, $sql);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $session Required. The session in which the SQL query should be performed.
+ * @param string $sql Required. The SQL query string.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type TransactionSelector $transaction
+ * The transaction to use. If none is provided, the default is a
+ * temporary read-only transaction with strong concurrency.
+ * @type Struct $params
+ * The SQL query string can contain parameter placeholders. A parameter
+ * placeholder consists of `'@'` followed by the parameter
+ * name. Parameter names consist of any combination of letters,
+ * numbers, and underscores.
+ *
+ * Parameters can appear anywhere that a literal value is expected. The same
+ * parameter name can be used more than once, for example:
+ * `"WHERE id > @msg_id AND id < @msg_id + 100"`
+ *
+ * It is an error to execute an SQL query with unbound parameters.
+ *
+ * Parameter values are specified using `params`, which is a JSON
+ * object whose keys are parameter names, and whose values are the
+ * corresponding parameter values.
+ * @type array $paramTypes
+ * It is not always possible for Cloud Spanner to infer the right SQL type
+ * from a JSON value. For example, values of type `BYTES` and values
+ * of type `STRING` both appear in [params][google.spanner.v1.ExecuteSqlRequest.params] as JSON strings.
+ *
+ * In these cases, `param_types` can be used to specify the exact
+ * SQL type for some or all of the SQL query parameters. See the
+ * definition of [Type][google.spanner.v1.Type] for more information
+ * about SQL types.
+ * @type string $resumeToken
+ * If this request is resuming a previously interrupted SQL query
+ * execution, `resume_token` should be copied from the last
+ * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this
+ * enables the new SQL query execution to resume where the last one left
+ * off. The rest of the request parameters must exactly match the
+ * request that yielded this token.
+ * @type QueryMode $queryMode
+ * Used to control the amount of debugging information returned in
+ * [ResultSetStats][google.spanner.v1.ResultSetStats].
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\v1\ResultSet
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function executeSql($session, $sql, $optionalArgs = [])
+ {
+ $request = new ExecuteSqlRequest();
+ $request->setSession($session);
+ $request->setSql($sql);
+ if (isset($optionalArgs['transaction'])) {
+ $request->setTransaction($optionalArgs['transaction']);
+ }
+ if (isset($optionalArgs['params'])) {
+ $request->setParams($optionalArgs['params']);
+ }
+ if (isset($optionalArgs['paramTypes'])) {
+ foreach ($optionalArgs['paramTypes'] as $key => $value) {
+ $request->addParamTypes((new ParamTypesEntry())->setKey($key)->setValue($value));
+ }
+ }
+ if (isset($optionalArgs['resumeToken'])) {
+ $request->setResumeToken($optionalArgs['resumeToken']);
+ }
+ if (isset($optionalArgs['queryMode'])) {
+ $request->setQueryMode($optionalArgs['queryMode']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['executeSql']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'ExecuteSql',
+ $mergedSettings,
+ $this->descriptors['executeSql']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Like [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], except returns the result
+ * set as a stream. Unlike [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], there
+ * is no limit on the size of the returned result set. However, no
+ * individual row in the result set can exceed 100 MiB, and no
+ * column value can exceed 10 MiB.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $sql = "";
+ * // Read all responses until the stream is complete
+ * $stream = $spannerClient->executeStreamingSql($formattedSession, $sql);
+ * foreach ($stream->readAll() as $element) {
+ * // doSomethingWith($element);
+ * }
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $session Required. The session in which the SQL query should be performed.
+ * @param string $sql Required. The SQL query string.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type TransactionSelector $transaction
+ * The transaction to use. If none is provided, the default is a
+ * temporary read-only transaction with strong concurrency.
+ * @type Struct $params
+ * The SQL query string can contain parameter placeholders. A parameter
+ * placeholder consists of `'@'` followed by the parameter
+ * name. Parameter names consist of any combination of letters,
+ * numbers, and underscores.
+ *
+ * Parameters can appear anywhere that a literal value is expected. The same
+ * parameter name can be used more than once, for example:
+ * `"WHERE id > @msg_id AND id < @msg_id + 100"`
+ *
+ * It is an error to execute an SQL query with unbound parameters.
+ *
+ * Parameter values are specified using `params`, which is a JSON
+ * object whose keys are parameter names, and whose values are the
+ * corresponding parameter values.
+ * @type array $paramTypes
+ * It is not always possible for Cloud Spanner to infer the right SQL type
+ * from a JSON value. For example, values of type `BYTES` and values
+ * of type `STRING` both appear in [params][google.spanner.v1.ExecuteSqlRequest.params] as JSON strings.
+ *
+ * In these cases, `param_types` can be used to specify the exact
+ * SQL type for some or all of the SQL query parameters. See the
+ * definition of [Type][google.spanner.v1.Type] for more information
+ * about SQL types.
+ * @type string $resumeToken
+ * If this request is resuming a previously interrupted SQL query
+ * execution, `resume_token` should be copied from the last
+ * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this
+ * enables the new SQL query execution to resume where the last one left
+ * off. The rest of the request parameters must exactly match the
+ * request that yielded this token.
+ * @type QueryMode $queryMode
+ * Used to control the amount of debugging information returned in
+ * [ResultSetStats][google.spanner.v1.ResultSetStats].
+ * @type int $timeoutMillis
+ * Timeout to use for this call.
+ * }
+ *
+ * @return \Google\GAX\ServerStreamingResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function executeStreamingSql($session, $sql, $optionalArgs = [])
+ {
+ $request = new ExecuteSqlRequest();
+ $request->setSession($session);
+ $request->setSql($sql);
+ if (isset($optionalArgs['transaction'])) {
+ $request->setTransaction($optionalArgs['transaction']);
+ }
+ if (isset($optionalArgs['params'])) {
+ $request->setParams($optionalArgs['params']);
+ }
+ if (isset($optionalArgs['paramTypes'])) {
+ foreach ($optionalArgs['paramTypes'] as $key => $value) {
+ $request->addParamTypes((new ParamTypesEntry())->setKey($key)->setValue($value));
+ }
+ }
+ if (isset($optionalArgs['resumeToken'])) {
+ $request->setResumeToken($optionalArgs['resumeToken']);
+ }
+ if (isset($optionalArgs['queryMode'])) {
+ $request->setQueryMode($optionalArgs['queryMode']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['executeStreamingSql']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'ExecuteStreamingSql',
+ $mergedSettings,
+ $this->descriptors['executeStreamingSql']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Reads rows from the database using key lookups and scans, as a
+ * simple key/value style alternative to
+ * [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql]. This method cannot be used to
+ * return a result set larger than 10 MiB; if the read matches more
+ * data than that, the read fails with a `FAILED_PRECONDITION`
+ * error.
+ *
+ * Reads inside read-write transactions might return `ABORTED`. If
+ * this occurs, the application should restart the transaction from
+ * the beginning. See [Transaction][google.spanner.v1.Transaction] for more details.
+ *
+ * Larger result sets can be yielded in streaming fashion by calling
+ * [StreamingRead][google.spanner.v1.Spanner.StreamingRead] instead.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $table = "";
+ * $columns = [];
+ * $keySet = new KeySet();
+ * $response = $spannerClient->read($formattedSession, $table, $columns, $keySet);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $session Required. The session in which the read should be performed.
+ * @param string $table Required. The name of the table in the database to be read.
+ * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching
+ * this request.
+ * @param KeySet $keySet Required. `key_set` identifies the rows to be yielded. `key_set` names the
+ * primary keys of the rows in [table][google.spanner.v1.ReadRequest.table] to be yielded, unless [index][google.spanner.v1.ReadRequest.index]
+ * is present. If [index][google.spanner.v1.ReadRequest.index] is present, then [key_set][google.spanner.v1.ReadRequest.key_set] instead names
+ * index keys in [index][google.spanner.v1.ReadRequest.index].
+ *
+ * Rows are yielded in table primary key order (if [index][google.spanner.v1.ReadRequest.index] is empty)
+ * or index key order (if [index][google.spanner.v1.ReadRequest.index] is non-empty).
+ *
+ * It is not an error for the `key_set` to name rows that do not
+ * exist in the database. Read yields nothing for nonexistent rows.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type TransactionSelector $transaction
+ * The transaction to use. If none is provided, the default is a
+ * temporary read-only transaction with strong concurrency.
+ * @type string $index
+ * If non-empty, the name of an index on [table][google.spanner.v1.ReadRequest.table]. This index is
+ * used instead of the table primary key when interpreting [key_set][google.spanner.v1.ReadRequest.key_set]
+ * and sorting result rows. See [key_set][google.spanner.v1.ReadRequest.key_set] for further information.
+ * @type int $limit
+ * If greater than zero, only the first `limit` rows are yielded. If `limit`
+ * is zero, the default is no limit.
+ * @type string $resumeToken
+ * If this request is resuming a previously interrupted read,
+ * `resume_token` should be copied from the last
+ * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this
+ * enables the new read to resume where the last read left off. The
+ * rest of the request parameters must exactly match the request
+ * that yielded this token.
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\v1\ResultSet
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function read($session, $table, $columns, $keySet, $optionalArgs = [])
+ {
+ $request = new ReadRequest();
+ $request->setSession($session);
+ $request->setTable($table);
+ foreach ($columns as $elem) {
+ $request->addColumns($elem);
+ }
+ $request->setKeySet($keySet);
+ if (isset($optionalArgs['transaction'])) {
+ $request->setTransaction($optionalArgs['transaction']);
+ }
+ if (isset($optionalArgs['index'])) {
+ $request->setIndex($optionalArgs['index']);
+ }
+ if (isset($optionalArgs['limit'])) {
+ $request->setLimit($optionalArgs['limit']);
+ }
+ if (isset($optionalArgs['resumeToken'])) {
+ $request->setResumeToken($optionalArgs['resumeToken']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['read']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'Read',
+ $mergedSettings,
+ $this->descriptors['read']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Like [Read][google.spanner.v1.Spanner.Read], except returns the result set as a
+ * stream. Unlike [Read][google.spanner.v1.Spanner.Read], there is no limit on the
+ * size of the returned result set. However, no individual row in
+ * the result set can exceed 100 MiB, and no column value can exceed
+ * 10 MiB.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $table = "";
+ * $columns = [];
+ * $keySet = new KeySet();
+ * // Read all responses until the stream is complete
+ * $stream = $spannerClient->streamingRead($formattedSession, $table, $columns, $keySet);
+ * foreach ($stream->readAll() as $element) {
+ * // doSomethingWith($element);
+ * }
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $session Required. The session in which the read should be performed.
+ * @param string $table Required. The name of the table in the database to be read.
+ * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching
+ * this request.
+ * @param KeySet $keySet Required. `key_set` identifies the rows to be yielded. `key_set` names the
+ * primary keys of the rows in [table][google.spanner.v1.ReadRequest.table] to be yielded, unless [index][google.spanner.v1.ReadRequest.index]
+ * is present. If [index][google.spanner.v1.ReadRequest.index] is present, then [key_set][google.spanner.v1.ReadRequest.key_set] instead names
+ * index keys in [index][google.spanner.v1.ReadRequest.index].
+ *
+ * Rows are yielded in table primary key order (if [index][google.spanner.v1.ReadRequest.index] is empty)
+ * or index key order (if [index][google.spanner.v1.ReadRequest.index] is non-empty).
+ *
+ * It is not an error for the `key_set` to name rows that do not
+ * exist in the database. Read yields nothing for nonexistent rows.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type TransactionSelector $transaction
+ * The transaction to use. If none is provided, the default is a
+ * temporary read-only transaction with strong concurrency.
+ * @type string $index
+ * If non-empty, the name of an index on [table][google.spanner.v1.ReadRequest.table]. This index is
+ * used instead of the table primary key when interpreting [key_set][google.spanner.v1.ReadRequest.key_set]
+ * and sorting result rows. See [key_set][google.spanner.v1.ReadRequest.key_set] for further information.
+ * @type int $limit
+ * If greater than zero, only the first `limit` rows are yielded. If `limit`
+ * is zero, the default is no limit.
+ * @type string $resumeToken
+ * If this request is resuming a previously interrupted read,
+ * `resume_token` should be copied from the last
+ * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this
+ * enables the new read to resume where the last read left off. The
+ * rest of the request parameters must exactly match the request
+ * that yielded this token.
+ * @type int $timeoutMillis
+ * Timeout to use for this call.
+ * }
+ *
+ * @return \Google\GAX\ServerStreamingResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function streamingRead($session, $table, $columns, $keySet, $optionalArgs = [])
+ {
+ $request = new ReadRequest();
+ $request->setSession($session);
+ $request->setTable($table);
+ foreach ($columns as $elem) {
+ $request->addColumns($elem);
+ }
+ $request->setKeySet($keySet);
+ if (isset($optionalArgs['transaction'])) {
+ $request->setTransaction($optionalArgs['transaction']);
+ }
+ if (isset($optionalArgs['index'])) {
+ $request->setIndex($optionalArgs['index']);
+ }
+ if (isset($optionalArgs['limit'])) {
+ $request->setLimit($optionalArgs['limit']);
+ }
+ if (isset($optionalArgs['resumeToken'])) {
+ $request->setResumeToken($optionalArgs['resumeToken']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['streamingRead']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'StreamingRead',
+ $mergedSettings,
+ $this->descriptors['streamingRead']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Begins a new transaction. This step can often be skipped:
+ * [Read][google.spanner.v1.Spanner.Read], [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] and
+ * [Commit][google.spanner.v1.Spanner.Commit] can begin a new transaction as a
+ * side-effect.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $options = new TransactionOptions();
+ * $response = $spannerClient->beginTransaction($formattedSession, $options);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $session Required. The session in which the transaction runs.
+ * @param TransactionOptions $options Required. Options for the new transaction.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\v1\Transaction
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function beginTransaction($session, $options, $optionalArgs = [])
+ {
+ $request = new BeginTransactionRequest();
+ $request->setSession($session);
+ $request->setOptions($options);
+
+ $mergedSettings = $this->defaultCallSettings['beginTransaction']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'BeginTransaction',
+ $mergedSettings,
+ $this->descriptors['beginTransaction']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Commits a transaction. The request includes the mutations to be
+ * applied to rows in the database.
+ *
+ * `Commit` might return an `ABORTED` error. This can occur at any time;
+ * commonly, the cause is conflicts with concurrent
+ * transactions. However, it can also happen for a variety of other
+ * reasons. If `Commit` returns `ABORTED`, the caller should re-attempt
+ * the transaction from the beginning, re-using the same session.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $mutations = [];
+ * $response = $spannerClient->commit($formattedSession, $mutations);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $session Required. The session in which the transaction to be committed is running.
+ * @param Mutation[] $mutations The mutations to be executed when this transaction commits. All
+ * mutations are applied atomically, in the order they appear in
+ * this list.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type string $transactionId
+ * Commit a previously-started transaction.
+ * @type TransactionOptions $singleUseTransaction
+ * Execute mutations in a temporary transaction. Note that unlike
+ * commit of a previously-started transaction, commit with a
+ * temporary transaction is non-idempotent. That is, if the
+ * `CommitRequest` is sent to Cloud Spanner more than once (for
+ * instance, due to retries in the application, or in the
+ * transport library), it is possible that the mutations are
+ * executed more than once. If this is undesirable, use
+ * [BeginTransaction][google.spanner.v1.Spanner.BeginTransaction] and
+ * [Commit][google.spanner.v1.Spanner.Commit] instead.
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @return \google\spanner\v1\CommitResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function commit($session, $mutations, $optionalArgs = [])
+ {
+ $request = new CommitRequest();
+ $request->setSession($session);
+ foreach ($mutations as $elem) {
+ $request->addMutations($elem);
+ }
+ if (isset($optionalArgs['transactionId'])) {
+ $request->setTransactionId($optionalArgs['transactionId']);
+ }
+ if (isset($optionalArgs['singleUseTransaction'])) {
+ $request->setSingleUseTransaction($optionalArgs['singleUseTransaction']);
+ }
+
+ $mergedSettings = $this->defaultCallSettings['commit']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'Commit',
+ $mergedSettings,
+ $this->descriptors['commit']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Rolls back a transaction, releasing any locks it holds. It is a good
+ * idea to call this for any transaction that includes one or more
+ * [Read][google.spanner.v1.Spanner.Read] or [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] requests and
+ * ultimately decides not to commit.
+ *
+ * `Rollback` returns `OK` if it successfully aborts the transaction, the
+ * transaction was already aborted, or the transaction is not
+ * found. `Rollback` never returns `ABORTED`.
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $spannerClient = new SpannerClient();
+ * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]");
+ * $transactionId = "";
+ * $spannerClient->rollback($formattedSession, $transactionId);
+ * } finally {
+ * $spannerClient->close();
+ * }
+ * ```
+ *
+ * @param string $session Required. The session in which the transaction to roll back is running.
+ * @param string $transactionId Required. The transaction to roll back.
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type \Google\GAX\RetrySettings $retrySettings
+ * Retry settings to use for this call. If present, then
+ * $timeoutMillis is ignored.
+ * @type int $timeoutMillis
+ * Timeout to use for this call. Only used if $retrySettings
+ * is not set.
+ * }
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function rollback($session, $transactionId, $optionalArgs = [])
+ {
+ $request = new RollbackRequest();
+ $request->setSession($session);
+ $request->setTransactionId($transactionId);
+
+ $mergedSettings = $this->defaultCallSettings['rollback']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->spannerStub,
+ 'Rollback',
+ $mergedSettings,
+ $this->descriptors['rollback']
+ );
+
+ return $callable(
+ $request,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
+ /**
+ * Initiates an orderly shutdown in which preexisting calls continue but new
+ * calls are immediately cancelled.
+ */
+ public function close()
+ {
+ $this->spannerStub->close();
+ }
+
+ private function createCredentialsCallback()
+ {
+ return $this->grpcCredentialsHelper->createCallCredentialsCallback();
+ }
+}
diff --git a/src/Spanner/V1/resources/spanner_client_config.json b/src/Spanner/V1/resources/spanner_client_config.json
new file mode 100644
index 000000000000..db4ced68c440
--- /dev/null
+++ b/src/Spanner/V1/resources/spanner_client_config.json
@@ -0,0 +1,78 @@
+{
+ "interfaces": {
+ "google.spanner.v1.Spanner": {
+ "retry_codes": {
+ "retry_codes_def": {
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": []
+ }
+ },
+ "retry_params": {
+ "default": {
+ "initial_retry_delay_millis": 1000,
+ "retry_delay_multiplier": 1.3,
+ "max_retry_delay_millis": 32000,
+ "initial_rpc_timeout_millis": 60000,
+ "rpc_timeout_multiplier": 1.0,
+ "max_rpc_timeout_millis": 60000,
+ "total_timeout_millis": 600000
+ }
+ },
+ "methods": {
+ "CreateSession": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "GetSession": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "DeleteSession": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "idempotent",
+ "retry_params_name": "default"
+ },
+ "ExecuteSql": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "ExecuteStreamingSql": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "Read": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "StreamingRead": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "BeginTransaction": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "Commit": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ },
+ "Rollback": {
+ "timeout_millis": 30000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Spanner/ValueInterface.php b/src/Spanner/ValueInterface.php
new file mode 100644
index 000000000000..e5e140d1c751
--- /dev/null
+++ b/src/Spanner/ValueInterface.php
@@ -0,0 +1,44 @@
+returnInt64AsObject = $returnInt64AsObject;
+ }
+
+ /**
+ * Accepts an array of key/value pairs, where the key is a SQL parameter
+ * name and the value is the value interpolated by the server, and returns
+ * an array of parameters and inferred parameter types.
+ *
+ * @param array $parameters The key/value parameters.
+ * @return array An associative array containing params and paramTypes.
+ */
+ public function formatParamsForExecuteSql(array $parameters)
+ {
+ $paramTypes = [];
+
+ foreach ($parameters as $key => $value) {
+ list ($parameters[$key], $paramTypes[$key]) = $this->paramType($value);
+ }
+
+ return [
+ 'params' => $parameters,
+ 'paramTypes' => $paramTypes
+ ];
+ }
+
+ /**
+ * Accepts a list of values and encodes the value into a format accepted by
+ * the Spanner API.
+ *
+ * @param array $values The list of values
+ * @return array The encoded values
+ */
+ public function encodeValuesAsSimpleType(array $values)
+ {
+ $res = [];
+ foreach ($values as $value) {
+ $res[] = $this->paramType($value)[0];
+ }
+
+ return $res;
+ }
+
+ /**
+ * Accepts a list of columns (with name and type) and a row from read or
+ * executeSql and decodes each value to its corresponding PHP type.
+ *
+ * @param array $columns The list of columns
+ * @param array $row The row data.
+ * @return array The decoded row data.
+ */
+ public function decodeValues(array $columns, array $row, $extractResult = false)
+ {
+ $cols = [];
+ $types = [];
+
+ foreach ($columns as $index => $column) {
+ $cols[] = (isset($column['name']))
+ ? $column['name']
+ : $index;
+ $types[] = $column['type'];
+ }
+
+ $res = [];
+ foreach ($row as $index => $value) {
+ $i = $cols[$index];
+ $res[$i] = $this->decodeValue($value, $types[$index]);
+ }
+
+ return $res;
+ }
+
+ /**
+ * Convert a timestamp string to a Timestamp class with nanosecond support.
+ *
+ * @param string $timestamp The timestamp string
+ * @return Timestamp
+ */
+ public function createTimestampWithNanos($timestamp)
+ {
+ $matches = [];
+ preg_match(self::NANO_REGEX, $timestamp, $matches);
+ $timestamp = preg_replace(self::NANO_REGEX, '.000000Z', $timestamp);
+
+ $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $timestamp);
+ return new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0);
+ }
+
+ /**
+ * Convert a single value to its corresponding PHP type.
+ *
+ * @param mixed $value The value to decode
+ * @param array $type The value type
+ * @return mixed
+ */
+ private function decodeValue($value, array $type)
+ {
+ switch ($type['code']) {
+ case self::TYPE_INT64:
+ $value = $this->returnInt64AsObject
+ ? new Int64($value)
+ : (int) $value;
+ break;
+
+ case self::TYPE_TIMESTAMP:
+ $value = $this->createTimestampWithNanos($value);
+ break;
+
+ case self::TYPE_DATE:
+ $value = new Date(new \DateTimeImmutable($value));
+ break;
+
+ case self::TYPE_BYTES:
+ $value = new Bytes(base64_decode($value));
+ break;
+
+ case self::TYPE_ARRAY:
+ $res = [];
+ foreach ($value as $item) {
+ $res[] = $this->decodeValue($item, $type['arrayElementType']);
+ }
+
+ $value = $res;
+ break;
+
+ case self::TYPE_STRUCT:
+ $value = $this->decodeValues($type['structType']['fields'], $value, true);
+ break;
+
+ case self::TYPE_FLOAT64:
+ // NaN, Infinite and -Infinite are possible FLOAT64 values,
+ // but when the gRPC response is decoded, they are represented
+ // as strings. This conditional checks for a string, converts to
+ // an equivalent double value, or dies if something really weird
+ // happens.
+ if (is_string($value)) {
+ switch ($value) {
+ case 'NaN':
+ $value = NAN;
+ break;
+
+ case 'Infinity':
+ $value = INF;
+ break;
+
+ case '-Infinity':
+ $value = -INF;
+ break;
+
+ default:
+ throw new \RuntimeException(sprintf(
+ 'Unexpected string value %s encountered in FLOAT64 field.',
+ $value
+ ));
+ }
+ }
+
+ break;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Create a spanner parameter type value object from a PHP value type.
+ *
+ * @param mixed $value The PHP value
+ * @return array The Value type
+ */
+ private function paramType($value)
+ {
+ $phpType = gettype($value);
+ switch ($phpType) {
+ case 'boolean':
+ $type = $this->typeObject(self::TYPE_BOOL);
+ break;
+
+ case 'integer':
+ $value = (string) $value;
+ $type = $this->typeObject(self::TYPE_INT64);
+ break;
+
+ case 'double':
+ $type = $this->typeObject(self::TYPE_FLOAT64);
+ break;
+
+ case 'string':
+ $type = $this->typeObject(self::TYPE_STRING);
+ break;
+
+ case 'resource':
+ $type = $this->typeObject(self::TYPE_BYTES);
+ $value = base64_encode(stream_get_contents($value));
+ break;
+
+ case 'object':
+ list ($type, $value) = $this->objectParam($value);
+ break;
+
+ case 'array':
+ if ($this->isAssoc($value)) {
+ throw new \InvalidArgumentException('Associative arrays are not supported');
+ }
+
+ $res = [];
+ $types = [];
+ foreach ($value as $element) {
+ $type = $this->paramType($element);
+ $res[] = $type[0];
+ $types[] = $type[1]['code'];
+ }
+
+ if (count(array_unique($types)) !== 1) {
+ throw new \InvalidArgumentException('Array values may not be of mixed type');
+ }
+
+ $type = $this->typeObject(
+ self::TYPE_ARRAY,
+ $this->typeObject($types[0]),
+ 'arrayElementType'
+ );
+
+ $value = $res;
+ break;
+
+ case 'NULL':
+ $type = null;
+ break;
+
+ default:
+ throw new \InvalidArgumentException(sprintf(
+ 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.',
+ $phpType
+ ));
+ break;
+ }
+
+ return [$value, $type];
+ }
+
+ private function objectParam($value)
+ {
+ if ($value instanceof ValueInterface) {
+ return [
+ $this->typeObject($value->type()),
+ $value->formatAsString()
+ ];
+ }
+
+ if ($value instanceof Int64) {
+ return [
+ $this->typeObject(self::TYPE_INT64),
+ $value->get()
+ ];
+ }
+
+ throw new \InvalidArgumentException(sprintf(
+ 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.',
+ get_class($value)
+ ));
+ }
+
+ private function typeObject($type, array $nestedDefinition = [], $nestedDefinitionType = null)
+ {
+ return array_filter([
+ 'code' => $type,
+ $nestedDefinitionType => $nestedDefinition
+ ]);
+ }
+}
diff --git a/src/Spanner/composer.json b/src/Spanner/composer.json
new file mode 100644
index 000000000000..c71551886e92
--- /dev/null
+++ b/src/Spanner/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "google/cloud-spanner",
+ "description": "Cloud Spanner Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "suggest": {
+ "google/gax": "Required to support gRPC",
+ "google/proto-client-php": "Required to support gRPC"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-spanner",
+ "target": "GoogleCloudPlatform/google-cloud-php-spanner.git",
+ "path": "src/Spanner",
+ "entry": "SpannerClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Spanner\\": ""
+ }
+ }
+}
diff --git a/src/Speech/Connection/Rest.php b/src/Speech/Connection/Rest.php
index e8b5aefae474..38985f03d90d 100644
--- a/src/Speech/Connection/Rest.php
+++ b/src/Speech/Connection/Rest.php
@@ -17,10 +17,11 @@
namespace Google\Cloud\Speech\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\RestTrait;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\UriTrait;
+use Google\Cloud\Speech\SpeechClient;
/**
* Implementation of the
@@ -39,7 +40,8 @@ class Rest implements ConnectionInterface
public function __construct(array $config = [])
{
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/speech-v1beta1.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/speech-v1beta1.json',
+ 'componentVersion' => SpeechClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
diff --git a/src/Speech/LICENSE b/src/Speech/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/Speech/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/Speech/Operation.php b/src/Speech/Operation.php
index c9731858d4bb..44e2f422401f 100644
--- a/src/Speech/Operation.php
+++ b/src/Speech/Operation.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Speech;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Speech\Connection\ConnectionInterface;
/**
@@ -25,10 +25,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Speech\SpeechClient;
*
- * $cloud = new ServiceBuilder();
- * $speech = $cloud->speech();
+ * $speech = new SpeechClient();
*
* $audioFileStream = fopen(__DIR__ . '/audio.flac', 'r');
* $operation = $speech->beginRecognizeOperation(
diff --git a/src/Speech/README.md b/src/Speech/README.md
new file mode 100644
index 000000000000..56c4890a46a0
--- /dev/null
+++ b/src/Speech/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP Speech
+
+> Idiomatic PHP client for [Cloud Speech](https://cloud.google.com/speech/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-speech/latest/speech/speechclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-speech
+```
diff --git a/src/Speech/SpeechClient.php b/src/Speech/SpeechClient.php
index 358211c820d5..fb986666c6be 100644
--- a/src/Speech/SpeechClient.php
+++ b/src/Speech/SpeechClient.php
@@ -17,17 +17,16 @@
namespace Google\Cloud\Speech;
-use Google\Cloud\ClientTrait;
+use Google\Cloud\Core\ClientTrait;
use Google\Cloud\Speech\Connection\ConnectionInterface;
use Google\Cloud\Speech\Connection\Rest;
use Google\Cloud\Storage\StorageObject;
use Psr\Cache\CacheItemPoolInterface;
/**
- * Google Cloud Speech client. The Cloud Speech API enables easy integration of
- * Google speech recognition technologies into developer applications. Send
- * audio and receive a text transcription from the Cloud Speech API service.
- * Find more information at the
+ * Google Cloud Speech enables easy integration of Google speech recognition
+ * technologies into developer applications. Send audio and receive a text
+ * transcription from the Cloud Speech API service. Find more information at the
* [Google Cloud Speech docs](https://cloud.google.com/speech/docs/).
*
* To enable better detection of encoding/sample rate values it is recommended
@@ -42,15 +41,6 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- *
- * $speech = $cloud->speech();
- * ```
- *
- * ```
- * // SpeechClient can be instantiated directly.
* use Google\Cloud\Speech\SpeechClient;
*
* $speech = new SpeechClient();
@@ -60,6 +50,8 @@ class SpeechClient
{
use ClientTrait;
+ const VERSION = '0.1.0';
+
const SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
/**
@@ -150,11 +142,18 @@ public function __construct(array $config = [])
* @see https://cloud.google.com/speech/docs/best-practices Speech API best practices
* @codingStandardsIgnoreEnd
*
- * @param resource|string|StorageObject $audio The audio to recognize. May
- * be a resource, string of bytes, or Google Cloud Storage object.
+ * @param resource|string|StorageObject $audio The audio to recognize. May
+ * be a resource, string of bytes, a URI pointing to a
+ * Google Cloud Storage object in the format of
+ * `gs://{bucket-name}/{object-name}` or a
+ * {@see Google\Cloud\Storage\StorageObject}.
* @param array $options [optional] {
* Configuration options.
*
+ * @type bool $detectGcsUri When providing $audio as a string, this flag
+ * determines whether or not to attempt to detect if the string
+ * represents a Google Cloud Storage URI in the format of
+ * `gs://{bucket-name}/{object-name}`. **Defaults to** `true`.
* @type int $sampleRate Sample rate in Hertz of the provided audio.
* Valid values are: 8000-48000. 16000 is optimal. For best
* results, set the sampling rate of the audio source to 16000 Hz.
@@ -264,10 +263,17 @@ public function recognize($audio, array $options = [])
* @codingStandardsIgnoreEnd
*
* @param resource|string|StorageObject $audio The audio to recognize. May
- * be a resource, string of bytes, or Google Cloud Storage object.
+ * be a resource, string of bytes, a URI pointing to a
+ * Google Cloud Storage object in the format of
+ * `gs://{bucket-name}/{object-name}` or a
+ * {@see Google\Cloud\Storage\StorageObject}.
* @param array $options [optional] {
* Configuration options.
*
+ * @type bool $detectGcsUri When providing $audio as a string, this flag
+ * determines whether or not to attempt to detect if the string
+ * represents a Google Cloud Storage URI in the format of
+ * `gs://{bucket-name}/{object-name}`. **Defaults to** `true`.
* @type int $sampleRate Sample rate in Hertz of the provided audio.
* Valid values are: 8000-48000. 16000 is optimal. For best
* results, set the sampling rate of the audio source to 16000 Hz.
@@ -343,6 +349,7 @@ private function formatRequest($audio, array $options)
{
$analyzedFileInfo = null;
$fileFormat = null;
+ $options += ['detectGcsUri' => true];
$recognizeOptions = [
'encoding',
'sampleRate',
@@ -353,16 +360,20 @@ private function formatRequest($audio, array $options)
];
if ($audio instanceof StorageObject) {
- $objIdentity = $audio->identity();
- $options['audio']['uri'] = 'gs://' . $objIdentity['bucket'] . '/' . $objIdentity['object'];
+ $options['audio']['uri'] = $audio->gcsUri();
$fileFormat = pathinfo($options['audio']['uri'], PATHINFO_EXTENSION);
} elseif (is_resource($audio)) {
$options['audio']['content'] = base64_encode(stream_get_contents($audio));
$fileFormat = pathinfo(stream_get_meta_data($audio)['uri'], PATHINFO_EXTENSION);
+ } elseif ($options['detectGcsUri'] && substr($audio, 0, 5) === 'gs://') {
+ $options['audio']['uri'] = $audio;
+ $fileFormat = pathinfo($options['audio']['uri'], PATHINFO_EXTENSION);
} else {
$options['audio']['content'] = base64_encode($audio);
}
+ unset($options['detectGcsUri']);
+
if (isset($options['encoding'])) {
$options['encoding'] = strtoupper($options['encoding']);
} else {
diff --git a/src/Speech/V1beta1/README.md b/src/Speech/V1beta1/README.md
new file mode 100644
index 000000000000..9d2f7afd62a2
--- /dev/null
+++ b/src/Speech/V1beta1/README.md
@@ -0,0 +1,5 @@
+# Cloud Speech
+
+Google Cloud Speech API enables developers to convert audio to text by applying powerful neural network models in an easy to use API.
+
+For more information, see [cloud.google.com](https://cloud.google.com/speech/).
diff --git a/src/Speech/V1beta1/SpeechClient.php b/src/Speech/V1beta1/SpeechClient.php
index 8bd0f7d4f1dc..2e2e7997c0d1 100644
--- a/src/Speech/V1beta1/SpeechClient.php
+++ b/src/Speech/V1beta1/SpeechClient.php
@@ -1,6 +1,6 @@
setEncoding($encoding);
+ * $config->setSampleRate($sampleRate);
+ * $uri = "gs://bucket_name/file_name.flac";
* $audio = new RecognitionAudio();
+ * $audio->setUri($uri);
* $response = $speechClient->syncRecognize($config, $audio);
* } finally {
* $speechClient->close();
@@ -92,7 +100,7 @@ class SpeechClient
/**
* The code generator version, to be included in the agent header.
*/
- const CODEGEN_VERSION = '0.1.0';
+ const CODEGEN_VERSION = '0.0.5';
private $grpcCredentialsHelper;
private $speechStub;
@@ -111,11 +119,62 @@ private static function getLongRunningDescriptors()
];
}
+ private static function getGrpcStreamingDescriptors()
+ {
+ return [
+ 'streamingRecognize' => [
+ 'grpcStreamingType' => 'BidiStreaming',
+ ],
+ ];
+ }
+
+ private static function getGapicVersion()
+ {
+ if (file_exists(__DIR__.'/../VERSION')) {
+ return trim(file_get_contents(__DIR__.'/../VERSION'));
+ } elseif (class_exists('\Google\Cloud\ServiceBuilder')) {
+ return \Google\Cloud\ServiceBuilder::VERSION;
+ } else {
+ return;
+ }
+ }
+
+ /**
+ * Return an OperationsClient object with the same endpoint as $this.
+ *
+ * @return \Google\GAX\LongRunning\OperationsClient
+ */
public function getOperationsClient()
{
return $this->operationsClient;
}
+ /**
+ * Resume an existing long running operation that was previously started
+ * by a long running API method. If $methodName is not provided, or does
+ * not match a long running API method, then the operation can still be
+ * resumed, but the OperationResponse object will not deserialize the
+ * final response.
+ *
+ * @param string $operationName The name of the long running operation
+ * @param string $methodName The name of the method used to start the operation
+ *
+ * @return \Google\GAX\OperationResponse
+ */
+ public function resumeOperation($operationName, $methodName = null)
+ {
+ $lroDescriptors = self::getLongRunningDescriptors();
+ if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) {
+ $options = $lroDescriptors[$methodName];
+ } else {
+ $options = [];
+ }
+ $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options);
+ $operation->reload();
+
+ return $operation;
+ }
+
// TODO(garrettjones): add channel (when supported in gRPC)
/**
* Constructor.
@@ -141,9 +200,6 @@ public function getOperationsClient()
* that don't use retries. For calls that use retries,
* set the timeout in RetryOptions.
* Default: 30000 (30 seconds)
- * @type string $appName The codename of the calling service. Default 'gax'.
- * @type string $appVersion The version of the calling service.
- * Default: the current version of GAX.
* @type \Google\Auth\CredentialsLoader $credentialsLoader
* A CredentialsLoader object created using the
* Google\Auth library.
@@ -159,34 +215,44 @@ public function __construct($options = [])
],
'retryingOverride' => null,
'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS,
- 'appName' => 'gax',
- 'appVersion' => AgentHeaderDescriptor::getGaxVersion(),
+ 'libName' => null,
+ 'libVersion' => null,
];
$options = array_merge($defaultOptions, $options);
- $this->operationsClient = new OperationsClient([
- 'serviceAddress' => $options['serviceAddress'],
- 'scopes' => $options['scopes'],
- ]);
+ if (array_key_exists('operationsClient', $options)) {
+ $this->operationsClient = $options['operationsClient'];
+ } else {
+ $this->operationsClient = new OperationsClient([
+ 'serviceAddress' => $options['serviceAddress'],
+ 'scopes' => $options['scopes'],
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ ]);
+ }
+
+ $gapicVersion = $options['libVersion'] ?: self::getGapicVersion();
$headerDescriptor = new AgentHeaderDescriptor([
- 'clientName' => $options['appName'],
- 'clientVersion' => $options['appVersion'],
- 'codeGenName' => self::CODEGEN_NAME,
- 'codeGenVersion' => self::CODEGEN_VERSION,
- 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(),
- 'phpVersion' => phpversion(),
+ 'libName' => $options['libName'],
+ 'libVersion' => $options['libVersion'],
+ 'gapicVersion' => $gapicVersion,
]);
$defaultDescriptors = ['headerDescriptor' => $headerDescriptor];
$this->descriptors = [
'syncRecognize' => $defaultDescriptors,
'asyncRecognize' => $defaultDescriptors,
+ 'streamingRecognize' => $defaultDescriptors,
];
$longRunningDescriptors = self::getLongRunningDescriptors();
foreach ($longRunningDescriptors as $method => $longRunningDescriptor) {
$this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient];
}
+ $grpcStreamingDescriptors = self::getGrpcStreamingDescriptors();
+ foreach ($grpcStreamingDescriptors as $method => $grpcStreamingDescriptor) {
+ $this->descriptors[$method]['grpcStreamingDescriptor'] = $grpcStreamingDescriptor;
+ }
$clientConfigJsonString = file_get_contents(__DIR__.'/resources/speech_client_config.json');
$clientConfig = json_decode($clientConfigJsonString, true);
@@ -230,8 +296,14 @@ public function __construct($options = [])
* ```
* try {
* $speechClient = new SpeechClient();
+ * $encoding = AudioEncoding::FLAC;
+ * $sampleRate = 44100;
* $config = new RecognitionConfig();
+ * $config->setEncoding($encoding);
+ * $config->setSampleRate($sampleRate);
+ * $uri = "gs://bucket_name/file_name.flac";
* $audio = new RecognitionAudio();
+ * $audio->setUri($uri);
* $response = $speechClient->syncRecognize($config, $audio);
* } finally {
* $speechClient->close();
@@ -288,8 +360,14 @@ public function syncRecognize($config, $audio, $optionalArgs = [])
* ```
* try {
* $speechClient = new SpeechClient();
+ * $encoding = AudioEncoding::FLAC;
+ * $sampleRate = 44100;
* $config = new RecognitionConfig();
+ * $config->setEncoding($encoding);
+ * $config->setSampleRate($sampleRate);
+ * $uri = "gs://bucket_name/file_name.flac";
* $audio = new RecognitionAudio();
+ * $audio->setUri($uri);
* $operationResponse = $speechClient->asyncRecognize($config, $audio);
* $operationResponse->pollUntilComplete();
* if ($operationResponse->operationSucceeded()) {
@@ -299,6 +377,23 @@ public function syncRecognize($config, $audio, $optionalArgs = [])
* $error = $operationResponse->getError();
* // handleError($error)
* }
+ *
+ * // OR start the operation, keep the operation name, and resume later
+ * $operationResponse = $speechClient->asyncRecognize($config, $audio);
+ * $operationName = $operationResponse->getName();
+ * // ... do other work
+ * $newOperationResponse = $speechClient->resumeOperation($operationName, 'asyncRecognize');
+ * while (!$newOperationResponse->isDone()) {
+ * // ... do other work
+ * $newOperationResponse->reload();
+ * }
+ * if ($newOperationResponse->operationSucceeded()) {
+ * $result = $newOperationResponse->getResult();
+ * // doSomethingWith($result)
+ * } else {
+ * $error = $newOperationResponse->getError();
+ * // handleError($error)
+ * }
* } finally {
* $speechClient->close();
* }
@@ -344,6 +439,75 @@ public function asyncRecognize($config, $audio, $optionalArgs = [])
['call_credentials_callback' => $this->createCredentialsCallback()]);
}
+ /**
+ * Perform bidirectional streaming speech-recognition: receive results while
+ * sending audio. This method is only available via the gRPC API (not REST).
+ *
+ * Sample code:
+ * ```
+ * try {
+ * $speechClient = new SpeechClient();
+ * $request = new StreamingRecognizeRequest();
+ * $requests = [$request];
+ *
+ * // Write all requests to the server, then read all responses until the
+ * // stream is complete
+ * $stream = $speechClient->streamingRecognize();
+ * $stream->writeAll($requests);
+ * foreach ($stream->closeWriteAndReadAll() as $element) {
+ * // doSomethingWith($element);
+ * }
+ *
+ * // OR write requests individually, making read() calls if
+ * // required. Call closeWrite() once writes are complete, and read the
+ * // remaining responses from the server.
+ * $stream = $speechClient->streamingRecognize();
+ * foreach ($requests as $request) {
+ * $stream->write($request);
+ * // if required, read a single response from the stream
+ * $element = $stream->read();
+ * // doSomethingWith($element)
+ * }
+ * $stream->closeWrite();
+ * $element = $stream->read();
+ * while (!is_null($element)) {
+ * // doSomethingWith($element)
+ * $element = $stream->read();
+ * }
+ * } finally {
+ * $speechClient->close();
+ * }
+ * ```
+ *
+ * @param array $optionalArgs {
+ * Optional.
+ *
+ * @type int $timeoutMillis
+ * Timeout to use for this call.
+ * }
+ *
+ * @return \Google\GAX\BidiStreamingResponse
+ *
+ * @throws \Google\GAX\ApiException if the remote call fails
+ */
+ public function streamingRecognize($optionalArgs = [])
+ {
+ $mergedSettings = $this->defaultCallSettings['streamingRecognize']->merge(
+ new CallSettings($optionalArgs)
+ );
+ $callable = ApiCallable::createApiCall(
+ $this->speechStub,
+ 'StreamingRecognize',
+ $mergedSettings,
+ $this->descriptors['streamingRecognize']
+ );
+
+ return $callable(
+ null,
+ [],
+ ['call_credentials_callback' => $this->createCredentialsCallback()]);
+ }
+
/**
* Initiates an orderly shutdown in which preexisting calls continue but new
* calls are immediately cancelled.
diff --git a/src/Speech/V1beta1/resources/speech_client_config.json b/src/Speech/V1beta1/resources/speech_client_config.json
index bbd5b8b62499..5d11ce19e587 100644
--- a/src/Speech/V1beta1/resources/speech_client_config.json
+++ b/src/Speech/V1beta1/resources/speech_client_config.json
@@ -2,13 +2,13 @@
"interfaces": {
"google.cloud.speech.v1beta1.Speech": {
"retry_codes": {
- "retry_codes_def": {
- "idempotent": [
- "DEADLINE_EXCEEDED",
- "UNAVAILABLE"
- ],
- "non_idempotent": []
- }
+ "idempotent": [
+ "DEADLINE_EXCEEDED",
+ "UNAVAILABLE"
+ ],
+ "non_idempotent": [
+ "UNAVAILABLE"
+ ]
},
"retry_params": {
"default": {
@@ -31,6 +31,11 @@
"timeout_millis": 60000,
"retry_codes_name": "idempotent",
"retry_params_name": "default"
+ },
+ "StreamingRecognize": {
+ "timeout_millis": 190000,
+ "retry_codes_name": "non_idempotent",
+ "retry_params_name": "default"
}
}
}
diff --git a/src/Speech/VERSION b/src/Speech/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/Speech/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/Speech/composer.json b/src/Speech/composer.json
new file mode 100644
index 000000000000..1e26c75b2f12
--- /dev/null
+++ b/src/Speech/composer.json
@@ -0,0 +1,27 @@
+{
+ "name": "google/cloud-speech",
+ "description": "Cloud Speech Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "suggest": {
+ "google/gax": "Required to support gRPC",
+ "google/proto-client-php": "Required to support gRPC",
+ "google/cloud-storage": "Run operations on files stored in Google Cloud Storage"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-speech",
+ "target": "GoogleCloudPlatform/google-cloud-php-speech.git",
+ "path": "src/Speech",
+ "entry": "SpeechClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Speech\\": ""
+ }
+ }
+}
diff --git a/src/Storage/Acl.php b/src/Storage/Acl.php
index c4014af5a58d..19d69150e497 100644
--- a/src/Storage/Acl.php
+++ b/src/Storage/Acl.php
@@ -29,10 +29,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Storage\StorageClient;
*
- * $cloud = new ServiceBuilder();
- * $storage = $cloud->storage();
+ * $storage = new StorageClient();
*
* $bucket = $storage->bucket('my-bucket');
* $acl = $bucket->acl();
diff --git a/src/Storage/Bucket.php b/src/Storage/Bucket.php
index 4c0c43443662..d8ae359c49dc 100644
--- a/src/Storage/Bucket.php
+++ b/src/Storage/Bucket.php
@@ -17,9 +17,12 @@
namespace Google\Cloud\Storage;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\Exception\ServiceException;
+use Google\Cloud\Core\Upload\ResumableUploader;
+use Google\Cloud\Core\Upload\StreamableUploader;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Storage\Connection\ConnectionInterface;
-use Google\Cloud\Upload\ResumableUploader;
use GuzzleHttp\Psr7;
use Psr\Http\Message\StreamInterface;
@@ -29,16 +32,16 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Storage\StorageClient;
*
- * $cloud = new ServiceBuilder();
- * $storage = $cloud->storage();
+ * $storage = new StorageClient();
*
* $bucket = $storage->bucket('my-bucket');
* ```
*/
class Bucket
{
+ use ArrayTrait;
use EncryptionTrait;
/**
@@ -182,7 +185,7 @@ public function exists()
* @see https://cloud.google.com/storage/docs/json_api/v1/objects/insert Objects insert API documentation.
* @see https://cloud.google.com/storage/docs/encryption#customer-supplied Customer-supplied encryption keys.
*
- * @param string|resource|StreamInterface $data The data to be uploaded.
+ * @param string|resource|StreamInterface|null $data The data to be uploaded.
* @param array $options [optional] {
* Configuration options.
*
@@ -198,10 +201,9 @@ public function exists()
* you have increased reliability at the risk of higher overhead.
* It is recommended to not use chunking.
* @type string $predefinedAcl Predefined ACL to apply to the object.
- * Acceptable values include,
- * `"authenticatedRead"`, `"bucketOwnerFullControl"`,
- * `"bucketOwnerRead"`, `"private"`, `"projectPrivate"`, and
- * `"publicRead"`.
+ * Acceptable values include, `"authenticatedRead"`,
+ * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
+ * `"projectPrivate"`, and `"publicRead"`.
* @type array $metadata The available options for metadata are outlined
* at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request-body).
* @type string $encryptionKey A base64 encoded AES-256 customer-supplied
@@ -265,7 +267,7 @@ public function upload($data, array $options = [])
* uploads.
* @see https://cloud.google.com/storage/docs/json_api/v1/objects/insert Objects insert API documentation.
*
- * @param string|resource|StreamInterface $data The data to be uploaded.
+ * @param string|resource|StreamInterface|null $data The data to be uploaded.
* @param array $options [optional] {
* Configuration options.
*
@@ -274,10 +276,6 @@ public function upload($data, array $options = [])
* applied using md5 hashing functionality. If true and the
* calculated hash does not match that of the upstream server the
* upload will be rejected.
- * @type int $chunkSize If provided the upload will be done in chunks.
- * The size must be in multiples of 262144 bytes. With chunking
- * you have increased reliability at the risk of higher overhead.
- * It is recommended to not use chunking.
* @type string $predefinedAcl Predefined ACL to apply to the object.
* Acceptable values include `"authenticatedRead`",
* `"bucketOwnerFullControl`", `"bucketOwnerRead`", `"private`",
@@ -310,6 +308,72 @@ public function getResumableUploader($data, array $options = [])
);
}
+ /**
+ * Get a streamable uploader which can provide greater control over the
+ * upload process. This is useful for generating large files and uploading
+ * the contents in chunks.
+ *
+ * Example:
+ * ```
+ * $uploader = $bucket->getStreamableUploader(
+ * 'initial contents',
+ * ['name' => 'data.txt']
+ * );
+ *
+ * // finish uploading the item
+ * $uploader->upload();
+ * ```
+ *
+ * @see https://cloud.google.com/storage/docs/json_api/v1/how-tos/upload#resumable Learn more about resumable
+ * uploads.
+ * @see https://cloud.google.com/storage/docs/json_api/v1/objects/insert Objects insert API documentation.
+ *
+ * @param string|resource|StreamInterface $data The data to be uploaded.
+ * @param array $options [optional] {
+ * Configuration options.
+ *
+ * @type string $name The name of the destination.
+ * @type bool $validate Indicates whether or not validation will be
+ * applied using md5 hashing functionality. If true and the
+ * calculated hash does not match that of the upstream server the
+ * upload will be rejected.
+ * @type int $chunkSize If provided the upload will be done in chunks.
+ * The size must be in multiples of 262144 bytes. With chunking
+ * you have increased reliability at the risk of higher overhead.
+ * It is recommended to not use chunking.
+ * @type string $predefinedAcl Predefined ACL to apply to the object.
+ * Acceptable values include, `"authenticatedRead"`,
+ * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
+ * `"projectPrivate"`, and `"publicRead"`.
+ * @type array $metadata The available options for metadata are outlined
+ * at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request-body).
+ * @type string $encryptionKey A base64 encoded AES-256 customer-supplied
+ * encryption key.
+ * @type string $encryptionKeySHA256 Base64 encoded SHA256 hash of the
+ * customer-supplied encryption key. This value will be calculated
+ * from the `encryptionKey` on your behalf if not provided, but
+ * for best performance it is recommended to pass in a cached
+ * version of the already calculated SHA.
+ * }
+ * @return StreamableUploader
+ * @throws \InvalidArgumentException
+ */
+ public function getStreamableUploader($data, array $options = [])
+ {
+ if (is_string($data) && !isset($options['name'])) {
+ throw new \InvalidArgumentException('A name is required when data is of type string.');
+ }
+
+ return $this->connection->insertObject(
+ $this->formatEncryptionHeaders($options) + [
+ 'bucket' => $this->identity['bucket'],
+ 'data' => $data,
+ 'streamable' => true,
+ 'validate' => false
+ ]
+ );
+ }
+
/**
* Lazily instantiates an object. There are no network requests made at this
* point. To see the operations that can be performed on an object please
@@ -382,41 +446,40 @@ public function object($name, array $options = [])
* prefixes are omitted.
* @type integer $maxResults Maximum number of results to return per
* request. Defaults to `1000`.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* @type string $prefix Filter results with this prefix.
* @type string $projection Determines which properties to return. May
- * be either 'full' or 'noAcl'.
+ * be either `"full"` or `"noAcl"`.
* @type bool $versions If true, lists all versions of an object as
* distinct results. The default is false.
* @type string $fields Selector which will cause the response to only
* return the specified fields.
* }
- * @return \Generator
+ * @return ObjectIterator
*/
public function objects(array $options = [])
{
- $options['pageToken'] = null;
- $includeVersions = isset($options['versions']) ? $options['versions'] : false;
-
- do {
- $response = $this->connection->listObjects($options + $this->identity);
-
- if (!array_key_exists('items', $response)) {
- break;
- }
-
- foreach ($response['items'] as $object) {
- $generation = $includeVersions ? $object['generation'] : null;
- yield new StorageObject(
- $this->connection,
- $object['name'],
- $this->identity['bucket'],
- $generation,
- $object
- );
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ $resultLimit = $this->pluck('resultLimit', $options, false);
+
+ return new ObjectIterator(
+ new ObjectPageIterator(
+ function (array $object) {
+ return new StorageObject(
+ $this->connection,
+ $object['name'],
+ $this->identity['bucket'],
+ isset($object['generation']) ? $object['generation'] : null,
+ $object
+ );
+ },
+ [$this->connection, 'listObjects'],
+ $options + $this->identity,
+ ['resultLimit' => $resultLimit]
+ )
+ );
}
/**
@@ -469,12 +532,14 @@ public function delete(array $options = [])
* @type string $ifMetagenerationNotMatch Makes the return of the bucket
* metadata conditional on whether the bucket's current
* metageneration does not match the given value.
- * @type string $predefinedAcl Apply a predefined set of access controls
- * to this bucket.
+ * @type string $predefinedAcl Predefined ACL to apply to the bucket.
+ * Acceptable values include, `"authenticatedRead"`,
+ * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
+ * `"projectPrivate"`, and `"publicRead"`.
* @type string $predefinedDefaultObjectAcl Apply a predefined set of
* default object access controls to this bucket.
* @type string $projection Determines which properties to return. May
- * be either 'full' or 'noAcl'.
+ * be either `"full"` or `"noAcl"`.
* @type string $fields Selector which will cause the response to only
* return the specified fields.
* @type array $acl Access controls on the bucket.
@@ -486,6 +551,11 @@ public function delete(array $options = [])
* @type array $logging The bucket's logging configuration, which
* defines the destination bucket and optional name prefix for the
* current bucket's logs.
+ * @type string $storageClass The bucket's storage class. This defines
+ * how objects in the bucket are stored and determines the SLA and
+ * the cost of storage. Acceptable values include
+ * `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`, `"COLDLINE"`,
+ * `"STANDARD"` and `"DURABLE_REDUCED_AVAILABILITY"`.
* @type array $versioning The bucket's versioning configuration.
* @type array $website The bucket's website configuration.
* }
@@ -613,7 +683,7 @@ public function compose(array $sourceObjects, $name, array $options = [])
* metadata conditional on whether the bucket's current
* metageneration does not match the given value.
* @type string $projection Determines which properties to return. May
- * be either 'full' or 'noAcl'.
+ * be either `"full"` or `"noAcl"`.
* }
* @return array
*/
@@ -648,7 +718,7 @@ public function info(array $options = [])
* metadata conditional on whether the bucket's current
* metageneration does not match the given value.
* @type string $projection Determines which properties to return. May
- * be either 'full' or 'noAcl'.
+ * be either `"full"` or `"noAcl"`.
* }
* @return array
*/
@@ -671,4 +741,34 @@ public function name()
{
return $this->identity['bucket'];
}
+
+ /**
+ * Returns whether the bucket with the given file prefix is writable.
+ * Tries to create a temporary file as a resumable upload which will
+ * not be completed (and cleaned up by GCS).
+ *
+ * @param string $file Optional file to try to write.
+ * @return boolean
+ * @throws ServiceException
+ */
+ public function isWritable($file = null)
+ {
+ $file = $file ?: '__tempfile';
+ $uploader = $this->getResumableUploader(
+ Psr7\stream_for(''),
+ ['name' => $file]
+ );
+ try {
+ $uploader->getResumeUri();
+ } catch (ServiceException $e) {
+ // We expect a 403 access denied error if the bucket is not writable
+ if ($e->getCode() == 403) {
+ return false;
+ }
+ // If not a 403, re-raise the unexpected error
+ throw $e;
+ }
+
+ return true;
+ }
}
diff --git a/src/Storage/Connection/Rest.php b/src/Storage/Connection/Rest.php
index 6e4ea62cea14..11c6f843ef64 100644
--- a/src/Storage/Connection/Rest.php
+++ b/src/Storage/Connection/Rest.php
@@ -17,14 +17,16 @@
namespace Google\Cloud\Storage\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\RestTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\Upload\AbstractUploader;
+use Google\Cloud\Core\Upload\MultipartUploader;
+use Google\Cloud\Core\Upload\ResumableUploader;
+use Google\Cloud\Core\Upload\StreamableUploader;
+use Google\Cloud\Core\UriTrait;
use Google\Cloud\Storage\Connection\ConnectionInterface;
-use Google\Cloud\Upload\AbstractUploader;
-use Google\Cloud\Upload\MultipartUploader;
-use Google\Cloud\Upload\ResumableUploader;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Storage\StorageClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
@@ -47,7 +49,8 @@ class Rest implements ConnectionInterface
public function __construct(array $config = [])
{
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/storage-v1.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/storage-v1.json',
+ 'componentVersion' => StorageClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
@@ -205,7 +208,7 @@ public function downloadObject(array $args = [])
];
$requestOptions = array_intersect_key($args, [
- 'httpOptions' => null,
+ 'restOptions' => null,
'retries' => null
]);
@@ -230,10 +233,16 @@ public function downloadObject(array $args = [])
public function insertObject(array $args = [])
{
$args = $this->resolveUploadOptions($args);
- $isResumable = $args['resumable'];
- $uploadType = $isResumable
- ? AbstractUploader::UPLOAD_TYPE_RESUMABLE
- : AbstractUploader::UPLOAD_TYPE_MULTIPART;
+
+ $uploadType = AbstractUploader::UPLOAD_TYPE_RESUMABLE;
+ if ($args['streamable']) {
+ $uploaderClass = StreamableUploader::class;
+ } elseif ($args['resumable']) {
+ $uploaderClass = ResumableUploader::class;
+ } else {
+ $uploaderClass = MultipartUploader::class;
+ $uploadType = AbstractUploader::UPLOAD_TYPE_MULTIPART;
+ }
$uriParams = [
'bucket' => $args['bucket'],
@@ -243,16 +252,7 @@ public function insertObject(array $args = [])
]
];
- if ($isResumable) {
- return new ResumableUploader(
- $this->requestWrapper,
- $args['data'],
- $this->expandUri(self::UPLOAD_URI, $uriParams),
- $args['uploaderOptions']
- );
- }
-
- return new MultipartUploader(
+ return new $uploaderClass(
$this->requestWrapper,
$args['data'],
$this->expandUri(self::UPLOAD_URI, $uriParams),
@@ -270,6 +270,7 @@ private function resolveUploadOptions(array $args)
'name' => null,
'validate' => true,
'resumable' => null,
+ 'streamable' => null,
'predefinedAcl' => null,
'metadata' => []
];
@@ -296,8 +297,9 @@ private function resolveUploadOptions(array $args)
: Psr7\mimetype_from_filename($args['metadata']['name']);
$uploaderOptionKeys = [
- 'httpOptions',
+ 'restOptions',
'retries',
+ 'requestTimeout',
'chunkSize',
'contentType',
'metadata'
diff --git a/src/Storage/Connection/ServiceDefinition/storage-v1.json b/src/Storage/Connection/ServiceDefinition/storage-v1.json
index 17c4e581fa9b..a689ca629d0d 100644
--- a/src/Storage/Connection/ServiceDefinition/storage-v1.json
+++ b/src/Storage/Connection/ServiceDefinition/storage-v1.json
@@ -1,4 +1,29 @@
{
+ "kind": "discovery#restDescription",
+ "etag": "\"tbys6C40o18GZwyMen5GMkdK-3s/HgbrZgh9zgUkvtwaM_qfO-xyD4k\"",
+ "discoveryVersion": "v1",
+ "id": "storage:v1",
+ "name": "storage",
+ "version": "v1",
+ "revision": "20170208",
+ "title": "Cloud Storage JSON API",
+ "description": "Stores and retrieves potentially large, immutable data objects.",
+ "ownerDomain": "google.com",
+ "ownerName": "Google",
+ "icons": {
+ "x16": "https://www.google.com/images/icons/product/cloud_storage-16.png",
+ "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png"
+ },
+ "documentationLink": "https://developers.google.com/storage/docs/json_api/",
+ "labels": [
+ "labs"
+ ],
+ "protocol": "rest",
+ "baseUrl": "https://www.googleapis.com/storage/v1/",
+ "basePath": "/storage/v1/",
+ "rootUrl": "https://www.googleapis.com/",
+ "servicePath": "storage/v1/",
+ "batchPath": "batch",
"parameters": {
"alt": {
"type": "string",
@@ -44,6 +69,27 @@
"location": "query"
}
},
+ "auth": {
+ "oauth2": {
+ "scopes": {
+ "https://www.googleapis.com/auth/cloud-platform": {
+ "description": "View and manage your data across Google Cloud Platform services"
+ },
+ "https://www.googleapis.com/auth/cloud-platform.read-only": {
+ "description": "View your data across Google Cloud Platform services"
+ },
+ "https://www.googleapis.com/auth/devstorage.full_control": {
+ "description": "Manage your data and permissions in Google Cloud Storage"
+ },
+ "https://www.googleapis.com/auth/devstorage.read_only": {
+ "description": "View your data in Google Cloud Storage"
+ },
+ "https://www.googleapis.com/auth/devstorage.read_write": {
+ "description": "Manage your data in Google Cloud Storage"
+ }
+ }
+ }
+ },
"schemas": {
"Bucket": {
"id": "Bucket",
@@ -110,7 +156,7 @@
},
"id": {
"type": "string",
- "description": "The ID of the bucket."
+ "description": "The ID of the bucket. For buckets, the id and name properities are the same."
},
"kind": {
"type": "string",
@@ -131,9 +177,13 @@
"type": "object",
"description": "The action to take.",
"properties": {
+ "storageClass": {
+ "type": "string",
+ "description": "Target storage class. Required iff the type of the action is SetStorageClass."
+ },
"type": {
"type": "string",
- "description": "Type of the action. Currently, only Delete is supported."
+ "description": "Type of the action. Currently, only Delete and SetStorageClass are supported."
}
}
},
@@ -155,6 +205,13 @@
"type": "boolean",
"description": "Relevant only for versioned objects. If the value is true, this condition matches live objects; if the value is false, it matches archived objects."
},
+ "matchesStorageClass": {
+ "type": "array",
+ "description": "Objects having any of the storage classes specified by this condition will be matched. Values include MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, STANDARD, and DURABLE_REDUCED_AVAILABILITY.",
+ "items": {
+ "type": "string"
+ }
+ },
"numNewerVersions": {
"type": "integer",
"description": "Relevant only for versioned objects. If the value is N, this condition is satisfied when there are at least N versions (including the live version) newer than this version of the object.",
@@ -224,7 +281,7 @@
},
"storageClass": {
"type": "string",
- "description": "The bucket's storage class. This defines how objects in the bucket are stored and determines the SLA and the cost of storage. Values include MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, STANDARD and DURABLE_REDUCED_AVAILABILITY. Defaults to STANDARD, which is equivalent to MULTI_REGIONAL or REGIONAL, depending on the bucket's location settings. For more information, see storage classes."
+ "description": "The bucket's default storage class, used whenever no storageClass is specified for a newly-created object. This defines how objects in the bucket are stored and determines the SLA and the cost of storage. Values include MULTI_REGIONAL, REGIONAL, STANDARD, NEARLINE, COLDLINE, and DURABLE_REDUCED_AVAILABILITY. If this value is not specified when the bucket is created, it will default to STANDARD. For more information, see storage classes."
},
"timeCreated": {
"type": "string",
@@ -248,15 +305,15 @@
},
"website": {
"type": "object",
- "description": "The bucket's website configuration.",
+ "description": "The bucket's website configuration, controlling how the service behaves when accessing bucket contents as a web site. See the Static Website Examples for more information.",
"properties": {
"mainPageSuffix": {
"type": "string",
- "description": "Behaves as the bucket's directory index where missing objects are treated as potential directories."
+ "description": "If the requested object path is missing, the service will ensure the path has a trailing '/', append this suffix, and attempt to retrieve the resulting object. This allows the creation of index.html objects to represent directory pages."
},
"notFoundPage": {
"type": "string",
- "description": "The custom object to return when a requested resource is not found."
+ "description": "If the requested object path is missing, and any mainPageSuffix object is missing, if applicable, the service will return the named object from this bucket as the content for a 404 Not Found result."
}
}
}
@@ -315,13 +372,13 @@
},
"team": {
"type": "string",
- "description": "The team. Can be owners, editors, or viewers."
+ "description": "The team."
}
}
},
"role": {
"type": "string",
- "description": "The access permission for the entity. Can be READER, WRITER, or OWNER.",
+ "description": "The access permission for the entity.",
"annotations": {
"required": [
"storage.bucketAccessControls.insert"
@@ -507,7 +564,7 @@
},
"cacheControl": {
"type": "string",
- "description": "Cache-Control directive for the object data."
+ "description": "Cache-Control directive for the object data. If omitted, and the object is accessible to all anonymous users, the default will be public, max-age=3600."
},
"componentCount": {
"type": "integer",
@@ -528,7 +585,7 @@
},
"contentType": {
"type": "string",
- "description": "Content-Type of the object data."
+ "description": "Content-Type of the object data. If contentType is not specified, object downloads will be served as application/octet-stream."
},
"crc32c": {
"type": "string",
@@ -559,7 +616,7 @@
},
"id": {
"type": "string",
- "description": "The ID of the object."
+ "description": "The ID of the object, including the bucket name, object name, and generation number."
},
"kind": {
"type": "string",
@@ -589,7 +646,7 @@
},
"name": {
"type": "string",
- "description": "The name of this object. Required if not specified by URL parameter."
+ "description": "The name of the object. Required if not specified by URL parameter."
},
"owner": {
"type": "object",
@@ -628,6 +685,11 @@
"description": "The deletion time of the object in RFC 3339 format. Will be returned if and only if this version of the object has been deleted.",
"format": "date-time"
},
+ "timeStorageClassUpdated": {
+ "type": "string",
+ "description": "The time at which the object's storage class was last changed. When the object is initially created, it will be set to timeCreated.",
+ "format": "date-time"
+ },
"updated": {
"type": "string",
"description": "The modification time of the object metadata in RFC 3339 format.",
@@ -654,7 +716,13 @@
},
"entity": {
"type": "string",
- "description": "The entity holding the permission, in one of the following forms: \n- user-userId \n- user-email \n- group-groupId \n- group-email \n- domain-domain \n- project-team-projectId \n- allUsers \n- allAuthenticatedUsers Examples: \n- The user liz@example.com would be user-liz@example.com. \n- The group example@googlegroups.com would be group-example@googlegroups.com. \n- To refer to all members of the Google Apps for Business domain example.com, the entity would be domain-example.com."
+ "description": "The entity holding the permission, in one of the following forms: \n- user-userId \n- user-email \n- group-groupId \n- group-email \n- domain-domain \n- project-team-projectId \n- allUsers \n- allAuthenticatedUsers Examples: \n- The user liz@example.com would be user-liz@example.com. \n- The group example@googlegroups.com would be group-example@googlegroups.com. \n- To refer to all members of the Google Apps for Business domain example.com, the entity would be domain-example.com.",
+ "annotations": {
+ "required": [
+ "storage.defaultObjectAccessControls.insert",
+ "storage.objectAccessControls.insert"
+ ]
+ }
},
"entityId": {
"type": "string",
@@ -666,7 +734,7 @@
},
"generation": {
"type": "string",
- "description": "The content generation of the object.",
+ "description": "The content generation of the object, if applied to an object.",
"format": "int64"
},
"id": {
@@ -680,7 +748,7 @@
},
"object": {
"type": "string",
- "description": "The name of the object."
+ "description": "The name of the object, if applied to an object."
},
"projectTeam": {
"type": "object",
@@ -692,13 +760,19 @@
},
"team": {
"type": "string",
- "description": "The team. Can be owners, editors, or viewers."
+ "description": "The team."
}
}
},
"role": {
"type": "string",
- "description": "The access permission for the entity. Can be READER or OWNER."
+ "description": "The access permission for the entity.",
+ "annotations": {
+ "required": [
+ "storage.defaultObjectAccessControls.insert",
+ "storage.objectAccessControls.insert"
+ ]
+ }
},
"selfLink": {
"type": "string",
@@ -715,7 +789,7 @@
"type": "array",
"description": "The list of items.",
"items": {
- "type": "any"
+ "$ref": "ObjectAccessControl"
}
},
"kind": {
@@ -1042,7 +1116,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit acl and defaultObjectAcl properties."
+ "Omit owner, acl and defaultObjectAcl properties."
],
"location": "query"
}
@@ -1122,7 +1196,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit acl and defaultObjectAcl properties."
+ "Omit owner, acl and defaultObjectAcl properties."
],
"location": "query"
}
@@ -1180,7 +1254,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit acl and defaultObjectAcl properties."
+ "Omit owner, acl and defaultObjectAcl properties."
],
"location": "query"
}
@@ -1203,7 +1277,7 @@
"id": "storage.buckets.patch",
"path": "b/{bucket}",
"httpMethod": "PATCH",
- "description": "Updates a bucket. This method supports patch semantics.",
+ "description": "Updates a bucket. Changes to the bucket will be readable immediately after writing, but configuration changes may take time to propagate. This method supports patch semantics.",
"parameters": {
"bucket": {
"type": "string",
@@ -1272,7 +1346,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit acl and defaultObjectAcl properties."
+ "Omit owner, acl and defaultObjectAcl properties."
],
"location": "query"
}
@@ -1288,15 +1362,14 @@
},
"scopes": [
"https://www.googleapis.com/auth/cloud-platform",
- "https://www.googleapis.com/auth/devstorage.full_control",
- "https://www.googleapis.com/auth/devstorage.read_write"
+ "https://www.googleapis.com/auth/devstorage.full_control"
]
},
"update": {
"id": "storage.buckets.update",
"path": "b/{bucket}",
"httpMethod": "PUT",
- "description": "Updates a bucket.",
+ "description": "Updates a bucket. Changes to the bucket will be readable immediately after writing, but configuration changes may take time to propagate.",
"parameters": {
"bucket": {
"type": "string",
@@ -1365,7 +1438,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit acl and defaultObjectAcl properties."
+ "Omit owner, acl and defaultObjectAcl properties."
],
"location": "query"
}
@@ -1381,8 +1454,7 @@
},
"scopes": [
"https://www.googleapis.com/auth/cloud-platform",
- "https://www.googleapis.com/auth/devstorage.full_control",
- "https://www.googleapis.com/auth/devstorage.read_write"
+ "https://www.googleapis.com/auth/devstorage.full_control"
]
}
}
@@ -2030,7 +2102,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit the acl property."
+ "Omit the owner, acl property."
],
"location": "query"
},
@@ -2189,7 +2261,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit the acl property."
+ "Omit the owner, acl property."
],
"location": "query"
}
@@ -2287,7 +2359,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit the acl property."
+ "Omit the owner, acl property."
],
"location": "query"
}
@@ -2368,7 +2440,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit the acl property."
+ "Omit the owner, acl property."
],
"location": "query"
},
@@ -2471,7 +2543,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit the acl property."
+ "Omit the owner, acl property."
],
"location": "query"
}
@@ -2488,8 +2560,7 @@
},
"scopes": [
"https://www.googleapis.com/auth/cloud-platform",
- "https://www.googleapis.com/auth/devstorage.full_control",
- "https://www.googleapis.com/auth/devstorage.read_write"
+ "https://www.googleapis.com/auth/devstorage.full_control"
]
},
"rewrite": {
@@ -2594,7 +2665,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit the acl property."
+ "Omit the owner, acl property."
],
"location": "query"
},
@@ -2718,7 +2789,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit the acl property."
+ "Omit the owner, acl property."
],
"location": "query"
}
@@ -2735,8 +2806,7 @@
},
"scopes": [
"https://www.googleapis.com/auth/cloud-platform",
- "https://www.googleapis.com/auth/devstorage.full_control",
- "https://www.googleapis.com/auth/devstorage.read_write"
+ "https://www.googleapis.com/auth/devstorage.full_control"
],
"supportsMediaDownload": true,
"useMediaDownloadService": true
@@ -2784,7 +2854,7 @@
],
"enumDescriptions": [
"Include all properties.",
- "Omit the acl property."
+ "Omit the owner, acl property."
],
"location": "query"
},
diff --git a/src/Storage/EncryptionTrait.php b/src/Storage/EncryptionTrait.php
index 694145782850..719079336e30 100644
--- a/src/Storage/EncryptionTrait.php
+++ b/src/Storage/EncryptionTrait.php
@@ -70,10 +70,10 @@ public function formatEncryptionHeaders(array $options)
+ $this->buildHeaders($destinationKey, $destinationKeySHA256, false);
if (!empty($encryptionHeaders)) {
- if (isset($options['httpOptions']['headers'])) {
- $options['httpOptions']['headers'] += $encryptionHeaders;
+ if (isset($options['restOptions']['headers'])) {
+ $options['restOptions']['headers'] += $encryptionHeaders;
} else {
- $options['httpOptions']['headers'] = $encryptionHeaders;
+ $options['restOptions']['headers'] = $encryptionHeaders;
}
}
diff --git a/src/Storage/LICENSE b/src/Storage/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/Storage/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/Storage/ObjectIterator.php b/src/Storage/ObjectIterator.php
new file mode 100644
index 000000000000..9e7cf2922256
--- /dev/null
+++ b/src/Storage/ObjectIterator.php
@@ -0,0 +1,39 @@
+pageIterator->prefixes();
+ }
+}
diff --git a/src/Storage/ObjectPageIterator.php b/src/Storage/ObjectPageIterator.php
new file mode 100644
index 000000000000..c50f80f88778
--- /dev/null
+++ b/src/Storage/ObjectPageIterator.php
@@ -0,0 +1,77 @@
+prefixes;
+ }
+
+ /**
+ * Get the current page.
+ *
+ * @return array|null
+ */
+ public function current()
+ {
+ if (!$this->page) {
+ $this->page = $this->executeCall();
+ }
+
+ if (isset($this->page['prefixes'])) {
+ $this->updatePrefixes();
+ }
+
+ return $this->get($this->itemsPath, $this->page);
+ }
+
+ /**
+ * Get the current page.
+ *
+ * @return array
+ */
+ private function updatePrefixes()
+ {
+ foreach ($this->page['prefixes'] as $prefix) {
+ if (!in_array($prefix, $this->prefixes)) {
+ $this->prefixes[] = $prefix;
+ }
+ }
+ }
+}
diff --git a/src/Storage/README.md b/src/Storage/README.md
new file mode 100644
index 000000000000..35e0f7f11107
--- /dev/null
+++ b/src/Storage/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP Storage
+
+> Idiomatic PHP client for [Cloud Storage](https://cloud.google.com/storage/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-storage/latest/storage/storageclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-storage
+```
diff --git a/src/Storage/ReadStream.php b/src/Storage/ReadStream.php
new file mode 100644
index 000000000000..33e55094ca4c
--- /dev/null
+++ b/src/Storage/ReadStream.php
@@ -0,0 +1,93 @@
+stream = $stream;
+ }
+
+ /**
+ * Return the full size of the buffer. If the underlying stream does
+ * not report it's size, try to fetch the size from the Content-Length
+ * response header.
+ *
+ * @return int The size of the stream.
+ */
+ public function getSize()
+ {
+ return $this->stream->getSize() ?: $this->getSizeFromMetadata();
+ }
+
+ /**
+ * Attempt to fetch the size from the Content-Length response header.
+ * If we cannot, return 0.
+ *
+ * @return int The Size of the stream
+ */
+ private function getSizeFromMetadata()
+ {
+ foreach ($this->stream->getMetadata('wrapper_data') as $value) {
+ if (substr($value, 0, 15) == "Content-Length:") {
+ return (int) substr($value, 16);
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Read bytes from the underlying buffer, retrying until we have read
+ * enough bytes or we cannot read any more. We do this because the
+ * internal C code for filling a buffer does not account for when
+ * we try to read large chunks from a user-land stream that does not
+ * return enough bytes.
+ *
+ * @param int $length The number of bytes to read.
+ * @return string Read bytes from the underlying stream.
+ */
+ public function read($length)
+ {
+ $data = '';
+ do {
+ $moreData = $this->stream->read($length);
+ $data .= $moreData;
+ $readLength = strlen($moreData);
+ $length -= $readLength;
+ } while ($length > 0 && $readLength > 0);
+
+ return $data;
+ }
+}
diff --git a/src/Storage/StorageClient.php b/src/Storage/StorageClient.php
index 8b64cffc8186..d7ca401647f4 100644
--- a/src/Storage/StorageClient.php
+++ b/src/Storage/StorageClient.php
@@ -17,27 +17,21 @@
namespace Google\Cloud\Storage;
-use Google\Cloud\ClientTrait;
+use Google\Cloud\Core\ArrayTrait;
+use Google\Cloud\Core\ClientTrait;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\Storage\Connection\ConnectionInterface;
use Google\Cloud\Storage\Connection\Rest;
use Psr\Cache\CacheItemPoolInterface;
/**
- * Google Cloud Storage client. Allows you to store and retrieve data on
- * Google's infrastructure. Find more information at
+ * Google Cloud Storage allows you to store and retrieve data on Google's
+ * infrastructure. Find more information at the
* [Google Cloud Storage API docs](https://developers.google.com/storage).
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- *
- * $storage = $cloud->storage();
- * ```
- *
- * ```
- * // StorageClient can be instantiated directly.
* use Google\Cloud\Storage\StorageClient;
*
* $storage = new StorageClient();
@@ -45,8 +39,11 @@
*/
class StorageClient
{
+ use ArrayTrait;
use ClientTrait;
+ const VERSION = '0.1.0';
+
const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/devstorage.full_control';
const READ_ONLY_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_only';
const READ_WRITE_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_write';
@@ -133,29 +130,34 @@ public function bucket($name)
* @param array $options [optional] {
* Configuration options.
*
- * @type integer $maxResults Maximum number of results to return per
- * request.
+ * @type int $maxResults Maximum number of results to return per
+ * requested page.
+ * @type int $resultLimit Limit the number of results returned in total.
+ * **Defaults to** `0` (return all results).
+ * @type string $pageToken A previously-returned page token used to
+ * resume the loading of results from a specific point.
* @type string $prefix Filter results with this prefix.
* @type string $projection Determines which properties to return. May
* be either 'full' or 'noAcl'.
* @type string $fields Selector which will cause the response to only
* return the specified fields.
* }
- * @return \Generator
+ * @return ItemIterator
*/
public function buckets(array $options = [])
{
- $options['pageToken'] = null;
+ $resultLimit = $this->pluck('resultLimit', $options, false);
- do {
- $response = $this->connection->listBuckets($options + ['project' => $this->projectId]);
-
- foreach ($response['items'] as $bucket) {
- yield new Bucket($this->connection, $bucket['name'], $bucket);
- }
-
- $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null;
- } while ($options['pageToken']);
+ return new ItemIterator(
+ new PageIterator(
+ function (array $bucket) {
+ return new Bucket($this->connection, $bucket['name'], $bucket);
+ },
+ [$this->connection, 'listBuckets'],
+ $options + ['project' => $this->projectId],
+ ['resultLimit' => $resultLimit]
+ )
+ );
}
/**
@@ -184,12 +186,16 @@ public function buckets(array $options = [])
* @param array $options [optional] {
* Configuration options.
*
- * @type string $predefinedAcl Apply a predefined set of access controls
- * to this bucket.
+ * @type string $predefinedAcl Predefined ACL to apply to the bucket.
+ * Acceptable values include, `"authenticatedRead"`,
+ * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
+ * `"projectPrivate"`, and `"publicRead"`.
* @type string $predefinedDefaultObjectAcl Apply a predefined set of
* default object access controls to this bucket.
* @type string $projection Determines which properties to return. May
- * be either 'full' or 'noAcl'.
+ * be either `"full"` or `"noAcl"`. **Defaults to** `"noAcl"`,
+ * unless the bucket resource specifies acl or defaultObjectAcl
+ * properties, when it defaults to `"full"`.
* @type string $fields Selector which will cause the response to only
* return the specified fields.
* @type array $acl Access controls on the bucket.
@@ -205,8 +211,10 @@ public function buckets(array $options = [])
* current bucket's logs.
* @type string $storageClass The bucket's storage class. This defines
* how objects in the bucket are stored and determines the SLA and
- * the cost of storage. Values include MULTI_REGIONAL, REGIONAL,
- * NEARLINE, COLDLINE, STANDARD and DURABLE_REDUCED_AVAILABILITY.
+ * the cost of storage. Acceptable values include
+ * `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`, `"COLDLINE"`,
+ * `"STANDARD"` and `"DURABLE_REDUCED_AVAILABILITY"`.
+ * **Defaults to** `STANDARD`.
* @type array $versioning The bucket's versioning configuration.
* @type array $website The bucket's website configuration.
* }
@@ -217,4 +225,25 @@ public function createBucket($name, array $options = [])
$response = $this->connection->insertBucket($options + ['name' => $name, 'project' => $this->projectId]);
return new Bucket($this->connection, $name, $response);
}
+
+ /**
+ * Registers this StorageClient as the handler for stream reading/writing.
+ *
+ * @param string $protocol The name of the protocol to use. **Defaults to** `gs`.
+ * @throws \RuntimeException
+ */
+ public function registerStreamWrapper($protocol = null)
+ {
+ return StreamWrapper::register($this, $protocol);
+ }
+
+ /**
+ * Unregisters the SteamWrapper
+ *
+ * @param string $protocol The name of the protocol to unregister. **Defaults to** `gs`.
+ */
+ public function unregisterStreamWrapper($protocol = null)
+ {
+ StreamWrapper::unregister($protocol);
+ }
}
diff --git a/src/Storage/StorageObject.php b/src/Storage/StorageObject.php
index bac0f32ce074..ab41c74289b3 100644
--- a/src/Storage/StorageObject.php
+++ b/src/Storage/StorageObject.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Storage;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Storage\Connection\ConnectionInterface;
use GuzzleHttp\Psr7;
use Psr\Http\Message\StreamInterface;
@@ -28,10 +28,9 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Storage\StorageClient;
*
- * $cloud = new ServiceBuilder();
- * $storage = $cloud->storage();
+ * $storage = new StorageClient();
*
* $bucket = $storage->bucket('my-bucket');
* $object = $bucket->object('my-object');
@@ -206,8 +205,10 @@ public function delete(array $options = [])
* @type string $ifMetagenerationNotMatch Makes the operation
* conditional on whether the object's current metageneration does
* not match the given value.
- * @type string $predefinedAcl Apply a predefined set of access controls
- * to this object.
+ * @type string $predefinedAcl Predefined ACL to apply to the object.
+ * Acceptable values include, `"authenticatedRead"`,
+ * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
+ * `"projectPrivate"`, and `"publicRead"`.
* @type string $projection Determines which properties to return. May
* be either 'full' or 'noAcl'.
* @type string $fields Selector which will cause the response to only
@@ -218,6 +219,12 @@ public function delete(array $options = [])
public function update(array $metadata, array $options = [])
{
$options += $metadata;
+
+ // can only set predefinedAcl or acl
+ if (isset($options['predefinedAcl'])) {
+ $options['acl'] = null;
+ }
+
return $this->info = $this->connection->patchObject($options + $this->identity);
}
@@ -252,11 +259,10 @@ public function update(array $metadata, array $options = [])
*
* @type string $name The name of the destination object. **Defaults
* to** the name of the source object.
- * @type string $predefinedAcl Access controls to apply to the
- * destination object. Acceptable values include
- * `authenticatedRead`, `bucketOwnerFullControl`,
- * `bucketOwnerRead`, `private`, `projectPrivate`, and
- * `publicRead`.
+ * @type string $predefinedAcl Predefined ACL to apply to the object.
+ * Acceptable values include, `"authenticatedRead"`,
+ * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
+ * `"projectPrivate"`, and `"publicRead"`.
* @type string $encryptionKey A base64 encoded AES-256 customer-supplied
* encryption key. It will be neccesary to provide this when a key
* was used during the object's creation.
@@ -359,11 +365,10 @@ public function copy($destination, array $options = [])
*
* @type string $name The name of the destination object. **Defaults
* to** the name of the source object.
- * @type string $predefinedAcl Access controls to apply to the
- * destination object. Acceptable values include
- * `authenticatedRead`, `bucketOwnerFullControl`,
- * `bucketOwnerRead`, `private`, `projectPrivate`, and
- * `publicRead`.
+ * @type string $predefinedAcl Predefined ACL to apply to the object.
+ * Acceptable values include, `"authenticatedRead"`,
+ * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
+ * `"projectPrivate"`, and `"publicRead"`.
* @type string $maxBytesRewrittenPerCall The maximum number of bytes
* that will be rewritten per rewrite request. Most callers
* shouldn't need to specify this parameter - it is primarily in
@@ -460,11 +465,10 @@ public function rewrite($destination, array $options = [])
* @param array $options [optional] {
* Configuration options.
*
- * @type string $predefinedAcl Access controls to apply to the
- * destination object. Acceptable values include
- * `authenticatedRead`, `bucketOwnerFullControl`,
- * `bucketOwnerRead`, `private`, `projectPrivate`, and
- * `publicRead`.
+ * @type string $predefinedAcl Predefined ACL to apply to the object.
+ * Acceptable values include, `"authenticatedRead"`,
+ * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
+ * `"projectPrivate"`, and `"publicRead"`.
* @type string $encryptionKey A base64 encoded AES-256 customer-supplied
* encryption key. It will be neccesary to provide this when a key
* was used during the object's creation.
@@ -497,18 +501,25 @@ public function rewrite($destination, array $options = [])
* @type string $ifSourceMetagenerationNotMatch Makes the operation
* conditional on whether the source object's current
* metageneration does not match the given value.
+ * @type string $destinationBucket Will move to this bucket if set. If
+ * not set, will default to the same bucket.
* }
* @return StorageObject The renamed object.
*/
public function rename($name, array $options = [])
{
- $copiedObject = $this->copy($this->identity['bucket'], [
+ $destinationBucket = isset($options['destinationBucket'])
+ ? $options['destinationBucket']
+ : $this->identity['bucket'];
+ unset($options['destinationBucket']);
+
+ $copiedObject = $this->copy($destinationBucket, [
'name' => $name
] + $options);
$this->delete(
array_intersect_key($options, [
- 'httpOptions' => null,
+ 'restOptions' => null,
'retries' => null
])
);
@@ -748,6 +759,26 @@ public function identity()
return $this->identity;
}
+ /**
+ * Formats the object as a string in the following format:
+ * `gs://{bucket-name}/{object-name}`.
+ *
+ * Example:
+ * ```
+ * echo $object->gcsUri();
+ * ```
+ *
+ * @return string
+ */
+ public function gcsUri()
+ {
+ return sprintf(
+ 'gs://%s/%s',
+ $this->identity['bucket'],
+ $this->identity['object']
+ );
+ }
+
/**
* Formats a destination based request, such as copy or rewrite.
*
diff --git a/src/Storage/StreamWrapper.php b/src/Storage/StreamWrapper.php
new file mode 100644
index 000000000000..f4a3dd824a2d
--- /dev/null
+++ b/src/Storage/StreamWrapper.php
@@ -0,0 +1,690 @@
+stream_close();
+ }
+
+ /**
+ * Register a StreamWrapper for reading and writing to Google Storage
+ *
+ * @param StorageClient $client The StorageClient configuration to use.
+ * @param string $protocol The name of the protocol to use. **Defaults to**
+ * `gs`.
+ * @throws \RuntimeException
+ */
+ public static function register(StorageClient $client, $protocol = null)
+ {
+ $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
+ if (!in_array($protocol, stream_get_wrappers())) {
+ if (!stream_wrapper_register($protocol, StreamWrapper::class, STREAM_IS_URL)) {
+ throw new \RuntimeException("Failed to register '$protocol://' protocol");
+ }
+ self::$clients[$protocol] = $client;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Unregisters the SteamWrapper
+ *
+ * @param string $protocol The name of the protocol to unregister. **Defaults
+ * to** `gs`.
+ */
+ public static function unregister($protocol = null)
+ {
+ $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
+ stream_wrapper_unregister($protocol);
+ unset(self::$clients[$protocol]);
+ }
+
+ /**
+ * Get the default client to use for streams.
+ *
+ * @param string $protocol The name of the protocol to get the client for.
+ * **Defaults to** `gs`.
+ * @return StorageClient
+ */
+ public static function getClient($protocol = null)
+ {
+ $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
+ return self::$clients[$protocol];
+ }
+
+ /**
+ * Callback handler for when a stream is opened. For reads, we need to
+ * download the file to see if it can be opened.
+ *
+ * @param string $path The path of the resource to open
+ * @param string $mode The fopen mode. Currently only supports ('r', 'rb', 'rt', 'w', 'wb', 'wt')
+ * @param int $flags Bitwise options STREAM_USE_PATH|STREAM_REPORT_ERRORS|STREAM_MUST_SEEK
+ * @param string $openedPath Will be set to the path on success if STREAM_USE_PATH option is set
+ * @return bool
+ */
+ public function stream_open($path, $mode, $flags, &$openedPath)
+ {
+ $client = $this->openPath($path);
+
+ // strip off 'b' or 't' from the mode
+ $mode = rtrim($mode, 'bt');
+
+ $options = [];
+ if ($this->context) {
+ $contextOptions = stream_context_get_options($this->context);
+ if (array_key_exists($this->protocol, $contextOptions)) {
+ $options = $contextOptions[$this->protocol] ?: [];
+ }
+ }
+
+ if ($mode == 'w') {
+ $this->stream = new WriteStream(null, $options);
+ $this->stream->setUploader(
+ $this->bucket->getStreamableUploader(
+ $this->stream,
+ $options + ['name' => $this->file]
+ )
+ );
+ } elseif ($mode == 'r') {
+ try {
+ // Lazy read from the source
+ $options['restOptions']['stream'] = true;
+ $this->stream = new ReadStream(
+ $this->bucket->object($this->file)->downloadAsStream($options)
+ );
+
+ // Wrap the response in a caching stream to make it seekable
+ if (!$this->stream->isSeekable() && ($flags & STREAM_MUST_SEEK)) {
+ $this->stream = new CachingStream($this->stream);
+ }
+ } catch (ServiceException $ex) {
+ return $this->returnError($ex->getMessage(), $flags);
+ }
+ } else {
+ return $this->returnError('Unknown stream_open mode.', $flags);
+ }
+
+ if ($flags & STREAM_USE_PATH) {
+ $openedPath = $path;
+ }
+ return true;
+ }
+
+ /**
+ * Callback handler for when we try to read a certain number of bytes.
+ *
+ * @param int $count The number of bytes to read.
+ *
+ * @return string
+ */
+ public function stream_read($count)
+ {
+ return $this->stream->read($count);
+ }
+
+ /**
+ * Callback handler for when we try to write data to the stream.
+ *
+ * @param string $data The data to write
+ *
+ * @return int The number of bytes written.
+ */
+ public function stream_write($data)
+ {
+ return $this->stream->write($data);
+ }
+
+ /**
+ * Callback handler for getting data about the stream.
+ *
+ * @return array
+ */
+ public function stream_stat()
+ {
+ $mode = $this->stream->isWritable()
+ ? self::FILE_WRITABLE_MODE
+ : self::FILE_READABLE_MODE;
+ return $this->makeStatArray([
+ 'mode' => $mode,
+ 'size' => $this->stream->getSize()
+ ]);
+ }
+
+ /**
+ * Callback handler for checking to see if the stream is at the end of file.
+ *
+ * @return bool
+ */
+ public function stream_eof()
+ {
+ return $this->stream->eof();
+ }
+
+ /**
+ * Callback handler for trying to close the stream.
+ */
+ public function stream_close()
+ {
+ if (isset($this->stream)) {
+ $this->stream->close();
+ }
+ }
+
+ /**
+ * Callback handler for trying to seek to a certain location in the stream.
+ *
+ * @param int $offset The stream offset to seek to
+ * @param int $whence Flag for what the offset is relative to. See:
+ * http://php.net/manual/en/streamwrapper.stream-seek.php
+ * @return bool
+ */
+ public function stream_seek($offset, $whence = SEEK_SET)
+ {
+ if ($this->stream->isSeekable()) {
+ $this->stream->seek($offset, $whence);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Callhack handler for inspecting our current position in the stream
+ *
+ * @return int
+ */
+ public function stream_tell()
+ {
+ return $this->stream->tell();
+ }
+
+ /**
+ * Callback handler for trying to close an opened directory.
+ *
+ * @return bool
+ */
+ public function dir_closedir()
+ {
+ return false;
+ }
+
+ /**
+ * Callback handler for trying to open a directory.
+ *
+ * @param string $path The url directory to open
+ * @param int $options Whether or not to enforce safe_mode
+ * @return bool
+ */
+ public function dir_opendir($path, $options)
+ {
+ $this->openPath($path);
+ return $this->dir_rewinddir();
+ }
+
+ /**
+ * Callback handler for reading an entry from a directory handle.
+ *
+ * @return string|bool
+ */
+ public function dir_readdir()
+ {
+ $object = $this->directoryIterator->current();
+ if ($object) {
+ $this->directoryIterator->next();
+ return $object->name();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Callback handler for rewind the directory handle.
+ *
+ * @return bool
+ */
+ public function dir_rewinddir()
+ {
+ try {
+ $this->directoryIterator = $this->bucket->objects([
+ 'prefix' => $this->file,
+ 'fields' => 'items/name,nextPageToken'
+ ]);
+ } catch (ServiceException $e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Callback handler for trying to create a directory. If no file path is specified,
+ * or STREAM_MKDIR_RECURSIVE option is set, then create the bucket if it does not exist.
+ *
+ * @param string $path The url directory to create
+ * @param int $mode The permissions on the directory
+ * @param int $options Bitwise mask of options. STREAM_MKDIR_RECURSIVE
+ * @return bool
+ */
+ public function mkdir($path, $mode, $options)
+ {
+ $path = $this->makeDirectory($path);
+ $client = $this->openPath($path);
+ $predefinedAcl = $this->determineAclFromMode($mode);
+
+ try {
+ if ($options & STREAM_MKDIR_RECURSIVE || $this->file == '') {
+ if (!$this->bucket->exists()) {
+ $client->createBucket($this->bucket->name(), [
+ 'predefinedAcl' => $predefinedAcl,
+ 'predefinedDefaultObjectAcl' => $predefinedAcl
+ ]);
+ }
+ }
+
+ // If the file name is empty, we were trying to create a bucket. In this case,
+ // don't create the placeholder file.
+ if ($this->file != '') {
+ // Fake a directory by creating an empty placeholder file whose name ends in '/'
+ $this->bucket->upload('', [
+ 'name' => $this->file,
+ 'predefinedAcl' => $predefinedAcl
+ ]);
+ }
+ } catch (ServiceException $e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Callback handler for trying to move a file or directory.
+ *
+ * @param string $from The URL to the current file
+ * @param string $to The URL of the new file location
+ * @return bool
+ */
+ public function rename($from, $to)
+ {
+ $url = (array) parse_url($to) + [
+ 'path' => '',
+ 'host' => ''
+ ];
+
+ $destinationBucket = $url['host'];
+ $destinationPath = substr($url['path'], 1);
+
+ $this->dir_opendir($from, []);
+ foreach ($this->directoryIterator as $file) {
+ $name = $file->name();
+ $newPath = str_replace($this->file, $destinationPath, $name);
+
+ $obj = $this->bucket->object($name);
+ try {
+ $obj->rename($newPath, ['destinationBucket' => $destinationBucket]);
+ } catch (ServiceException $e) {
+ // If any rename calls fail, abort and return false
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Callback handler for trying to remove a directory or a bucket. If the path is empty
+ * or '/', the bucket will be deleted.
+ *
+ * Note that the STREAM_MKDIR_RECURSIVE flag is ignored because the option cannot
+ * be set via the `rmdir()` function.
+ *
+ * @param string $path The URL directory to remove. If the path is empty or is '/',
+ * This will attempt to destroy the bucket.
+ * @param int $options Bitwise mask of options.
+ * @return bool
+ */
+ public function rmdir($path, $options)
+ {
+ $path = $this->makeDirectory($path);
+ $this->openPath($path);
+
+ try {
+ if ($this->file == '') {
+ $this->bucket->delete();
+ return true;
+ } else {
+ return $this->unlink($path);
+ }
+ } catch (ServiceException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Callback handler for retrieving the underlaying resource
+ *
+ * @param int $castAs STREAM_CAST_FOR_SELECT|STREAM_CAST_AS_STREAM
+ * @return resource|bool
+ */
+ public function stream_cast($castAs)
+ {
+ return false;
+ }
+
+ /**
+ * Callback handler for deleting a file
+ *
+ * @param string $path The URL of the file to delete
+ * @return bool
+ */
+ public function unlink($path)
+ {
+ $client = $this->openPath($path);
+ $object = $this->bucket->object($this->file);
+
+ try {
+ $object->delete();
+ return true;
+ } catch (ServiceException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Callback handler for retrieving information about a file
+ *
+ * @param string $path The URI to the file
+ * @param int $flags Bitwise mask of options
+ * @return array|bool
+ */
+ public function url_stat($path, $flags)
+ {
+ $client = $this->openPath($path);
+
+ // if directory
+ if ($this->isDirectory($this->file)) {
+ return $this->urlStatDirectory();
+ } else {
+ return $this->urlStatFile();
+ }
+ }
+
+ /**
+ * Parse the URL and set protocol, filename and bucket.
+ *
+ * @param string $path URL to open
+ * @return StorageClient
+ */
+ private function openPath($path)
+ {
+ $url = (array) parse_url($path) + [
+ 'scheme' => '',
+ 'path' => '',
+ 'host' => ''
+ ];
+ $this->protocol = $url['scheme'];
+ $this->file = ltrim($url['path'], '/');
+ $client = self::getClient($this->protocol);
+ $this->bucket = $client->bucket($url['host']);
+ return $client;
+ }
+
+ /**
+ * Given a path, ensure that we return a path that looks like a directory
+ *
+ * @param string $path
+ * @return string
+ */
+ private function makeDirectory($path)
+ {
+ if (substr($path, -1) == '/') {
+ return $path;
+ } else {
+ return $path . '/';
+ }
+ }
+
+ /**
+ * Calculate the `url_stat` response for a directory
+ *
+ * @return array|bool
+ */
+ private function urlStatDirectory()
+ {
+ $stats = [];
+ // 1. try to look up the directory as a file
+ try {
+ $this->object = $this->bucket->object($this->file);
+ $info = $this->object->info();
+
+ // equivalent to 40777 and 40444 in octal
+ $stats['mode'] = $this->bucket->isWritable()
+ ? self::DIRECTORY_WRITABLE_MODE
+ : self::DIRECTORY_READABLE_MODE;
+ $this->statsFromFileInfo($info, $stats);
+
+ return $this->makeStatArray($stats);
+ } catch (NotFoundException $e) {
+ } catch (ServiceException $e) {
+ return false;
+ }
+
+ // 2. try list files in that directory
+ try {
+ $objects = $this->bucket->objects([
+ 'prefix' => $this->file,
+ ]);
+
+ if (!$objects->current()) {
+ // can't list objects or doesn't exist
+ return false;
+ }
+ } catch (ServiceException $e) {
+ return false;
+ }
+
+ // equivalent to 40777 and 40444 in octal
+ $mode = $this->bucket->isWritable()
+ ? self::DIRECTORY_WRITABLE_MODE
+ : self::DIRECTORY_READABLE_MODE;
+ return $this->makeStatArray([
+ 'mode' => $mode
+ ]);
+ }
+
+ /**
+ * Calculate the `url_stat` response for a file
+ *
+ * @return array|bool
+ */
+ private function urlStatFile()
+ {
+ try {
+ $this->object = $this->bucket->object($this->file);
+ $info = $this->object->info();
+ } catch (ServiceException $e) {
+ // couldn't stat file
+ return false;
+ }
+
+ // equivalent to 100666 and 100444 in octal
+ $stats = array(
+ 'mode' => $this->bucket->isWritable()
+ ? self::FILE_WRITABLE_MODE
+ : self::FILE_READABLE_MODE
+ );
+ $this->statsFromFileInfo($info, $stats);
+ return $this->makeStatArray($stats);
+ }
+
+ /**
+ * Given a `StorageObject` info array, extract the available fields into the
+ * provided `$stats` array.
+ *
+ * @param array $info Array provided from a `StorageObject`.
+ * @param array $stats Array to put the calculated stats into.
+ */
+ private function statsFromFileInfo(array &$info, array &$stats)
+ {
+ $stats['size'] = (isset($info['size']))
+ ? (int) $info['size']
+ : null;
+
+ $stats['mtime'] = (isset($info['updated']))
+ ? strtotime($info['updated'])
+ : null;
+
+ $stats['ctime'] = (isset($info['timeCreated']))
+ ? strtotime($info['timeCreated'])
+ : null;
+ }
+
+ /**
+ * Return whether we think the provided path is a directory or not
+ *
+ * @param string $path
+ * @return bool
+ */
+ private function isDirectory($path)
+ {
+ return substr($path, -1) == '/';
+ }
+
+ /**
+ * Returns the associative array that a `stat()` response expects using the
+ * provided stats. Defaults the remaining fields to 0.
+ *
+ * @param array $stats Sparse stats entries to set.
+ * @return array
+ */
+ private function makeStatArray($stats)
+ {
+ return array_merge(
+ array_fill_keys([
+ 'dev',
+ 'ino',
+ 'mode',
+ 'nlink',
+ 'uid',
+ 'gid',
+ 'rdev',
+ 'size',
+ 'atime',
+ 'mtime',
+ 'ctime',
+ 'blksize',
+ 'blocks'
+ ], 0),
+ $stats
+ );
+ }
+
+ /**
+ * Helper for whether or not to trigger an error or just return false on an error.
+ *
+ * @param string $message The PHP error message to emit.
+ * @param int $flags Bitwise mask of options (STREAM_REPORT_ERRORS)
+ * @return bool Returns false
+ */
+ private function returnError($message, $flags)
+ {
+ if ($flags & STREAM_REPORT_ERRORS) {
+ trigger_error($message, E_USER_WARNING);
+ }
+ return false;
+ }
+
+ /**
+ * Helper for determining which predefinedAcl to use given a mode.
+ *
+ * @param int $mode Decimal representation of the file system permissions
+ * @return string
+ */
+ private function determineAclFromMode($mode)
+ {
+ if ($mode & 0004) {
+ // If any user can read, assume it should be publicRead.
+ return 'publicRead';
+ } elseif ($mode & 0040) {
+ // If any group user can read, assume it should be projectPrivate.
+ return 'projectPrivate';
+ } else {
+ // Otherwise, assume only the project/bucket owner can use the bucket.
+ return 'private';
+ }
+ }
+}
diff --git a/src/Storage/VERSION b/src/Storage/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/Storage/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/Storage/WriteStream.php b/src/Storage/WriteStream.php
new file mode 100644
index 000000000000..c78bcc91a89b
--- /dev/null
+++ b/src/Storage/WriteStream.php
@@ -0,0 +1,110 @@
+setUploader($uploader);
+ }
+ if (array_key_exists('chunkSize', $options)) {
+ $this->chunkSize = $options['chunkSize'];
+ }
+ $this->stream = new BufferStream($this->chunkSize);
+ }
+
+ /**
+ * Close the stream. Uploads any remaining data.
+ */
+ public function close()
+ {
+ if ($this->uploader && $this->hasWritten) {
+ $this->uploader->upload();
+ $this->uploader = null;
+ }
+ }
+
+ /**
+ * Write to the stream. If we pass the chunkable size, upload the available chunk.
+ *
+ * @param string $data Data to write
+ * @return int The number of bytes written
+ * @throws \RuntimeException
+ */
+ public function write($data)
+ {
+ if (!isset($this->uploader)) {
+ throw new \RuntimeException("No uploader set.");
+ }
+
+ // Ensure we have a resume uri here because we need to create the streaming
+ // upload before we have data (size of 0).
+ $this->uploader->getResumeUri();
+ $this->hasWritten = true;
+
+ if (!$this->stream->write($data)) {
+ $this->uploader->upload($this->getChunkedWriteSize());
+ }
+ return strlen($data);
+ }
+
+ /**
+ * Set the uploader for this class. You may need to set this after initialization
+ * if the uploader depends on this stream.
+ *
+ * @param AbstractUploader $uploader The new uploader to use.
+ */
+ public function setUploader($uploader)
+ {
+ $this->uploader = $uploader;
+ }
+
+ private function getChunkedWriteSize()
+ {
+ return (int) floor($this->getSize() / $this->chunkSize) * $this->chunkSize;
+ }
+}
diff --git a/src/Storage/composer.json b/src/Storage/composer.json
new file mode 100644
index 000000000000..d8d2daeeba43
--- /dev/null
+++ b/src/Storage/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "google/cloud-storage",
+ "description": "Cloud Storage Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-storage",
+ "target": "GoogleCloudPlatform/google-cloud-php-storage.git",
+ "path": "src/Storage",
+ "entry": "StorageClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Storage\\": ""
+ }
+ }
+}
diff --git a/src/Translate/Connection/Rest.php b/src/Translate/Connection/Rest.php
index 584163a29a0f..9e67617246c2 100644
--- a/src/Translate/Connection/Rest.php
+++ b/src/Translate/Connection/Rest.php
@@ -17,10 +17,11 @@
namespace Google\Cloud\Translate\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\RestTrait;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\UriTrait;
+use Google\Cloud\Translate\TranslateClient;
/**
* Implementation of the
@@ -39,7 +40,8 @@ class Rest implements ConnectionInterface
public function __construct(array $config = [])
{
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/translate-v2.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/translate-v2.json',
+ 'componentVersion' => TranslateClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
diff --git a/src/Translate/LICENSE b/src/Translate/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/Translate/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/Translate/README.md b/src/Translate/README.md
new file mode 100644
index 000000000000..da577d8ffea4
--- /dev/null
+++ b/src/Translate/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP Translation
+
+> Idiomatic PHP client for [Translation](https://cloud.google.com/translate/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-translate/latest/translate/translateclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-translate
+```
diff --git a/src/Translate/TranslateClient.php b/src/Translate/TranslateClient.php
index 086674a32e8e..aaf2b9649722 100644
--- a/src/Translate/TranslateClient.php
+++ b/src/Translate/TranslateClient.php
@@ -17,38 +17,31 @@
namespace Google\Cloud\Translate;
-use Google\Cloud\ClientTrait;
+use Google\Cloud\Core\ClientTrait;
use Google\Cloud\Translate\Connection\ConnectionInterface;
use Google\Cloud\Translate\Connection\Rest;
/**
- * Google Translate client. Provides the ability to dynamically
- * translate text between thousands of language pairs and lets websites and
- * programs integrate with the Google Cloud Translation API programmatically.
- * The Google Cloud Translation API is available as a paid service. See the
- * [Pricing](https://cloud.google.com/translation/v2/pricing) and
- * [FAQ](https://cloud.google.com/translation/v2/faq) pages for details. Find
- * more information at the
+ * Google Cloud Translation provides the ability to dynamically translate
+ * text between thousands of language pairs and lets websites and programs
+ * integrate with translation service programmatically. Find more
+ * information at the the
* [Google Cloud Translation docs](https://cloud.google.com/translation/docs/).
*
+ * The Google Cloud Translation API is available as a paid
+ * service. See the [Pricing](https://cloud.google.com/translation/v2/pricing)
+ * and [FAQ](https://cloud.google.com/translation/v2/faq) pages for details.
+ *
* Please note that while the Google Cloud Translation API supports
- * authentication via service account and application default credentials like
- * other Cloud Platform APIs, it also supports authentication via a public API
- * access key. If you wish to authenticate using an API key, follow the
+ * authentication via service account and application default credentials
+ * like other Cloud Platform APIs, it also supports authentication via a
+ * public API access key. If you wish to authenticate using an API key,
+ * follow the
* [before you begin](https://cloud.google.com/translation/v2/translating-text-with-rest#before-you-begin)
* instructions to learn how to generate a key.
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- *
- * $translate = $cloud->translate();
- * ```
- *
- * ```
- * // TranslateClient can be instantiated directly.
* use Google\Cloud\Translate\TranslateClient;
*
* $translate = new TranslateClient();
@@ -58,6 +51,8 @@ class TranslateClient
{
use ClientTrait;
+ const VERSION = '0.1.0';
+
const ENGLISH_LANGUAGE_CODE = 'en';
const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
@@ -203,7 +198,7 @@ public function translate($string, array $options = [])
* either plain-text or HTML. Acceptable values are `html` or
* `text`. **Defaults to** `"html"`.
* @type string $model The model to use for the translation request. May
- * be `nmt` or `base`. **Defaults to** an empty string.
+ * be `nmt` or `base`. **Defaults to** null.
* }
* @return array A set of translation results. Each result includes a
* `source` key containing the detected or provided language of the
@@ -213,15 +208,19 @@ public function translate($string, array $options = [])
public function translateBatch(array $strings, array $options = [])
{
$options += [
- 'model' => '',
+ 'model' => null,
];
- $response = $this->connection->listTranslations($options + [
+ $options = array_filter($options + [
'q' => $strings,
'key' => $this->key,
'target' => $this->targetLanguage,
'model' => $options['model']
- ]);
+ ], function ($opt) {
+ return !is_null($opt);
+ });
+
+ $response = $this->connection->listTranslations($options);
$translations = [];
$strings = array_values($strings);
diff --git a/src/Translate/VERSION b/src/Translate/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/Translate/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/Translate/composer.json b/src/Translate/composer.json
new file mode 100644
index 000000000000..a1439088d331
--- /dev/null
+++ b/src/Translate/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "google/cloud-translate",
+ "description": "Cloud Transation Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-translate",
+ "target": "GoogleCloudPlatform/google-cloud-php-translate.git",
+ "path": "src/Translate",
+ "entry": "TranslateClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Translate\\": ""
+ }
+ }
+}
diff --git a/src/Vision/Annotation.php b/src/Vision/Annotation.php
index 3187770600c9..6b2737a3dd5d 100644
--- a/src/Vision/Annotation.php
+++ b/src/Vision/Annotation.php
@@ -17,10 +17,13 @@
namespace Google\Cloud\Vision;
+use Google\Cloud\Vision\Annotation\CropHint;
+use Google\Cloud\Vision\Annotation\Document;
use Google\Cloud\Vision\Annotation\Entity;
use Google\Cloud\Vision\Annotation\Face;
use Google\Cloud\Vision\Annotation\ImageProperties;
use Google\Cloud\Vision\Annotation\SafeSearch;
+use Google\Cloud\Vision\Annotation\Web;
/**
* Represents a [Google Cloud Vision](https://cloud.google.com/vision) image
@@ -28,12 +31,11 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Vision\VisionClient;
*
- * $cloud = new ServiceBuilder();
- * $vision = $cloud->vision();
+ * $vision = new VisionClient();
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = $vision->image($imageResource, [
* 'FACE_DETECTION'
* ]);
@@ -49,42 +51,57 @@ class Annotation
private $info;
/**
- * @var array
+ * @var Face[]|null
*/
private $faces;
/**
- * @var array
+ * @var Entity[]|null
*/
private $landmarks;
/**
- * @var array
+ * @var Entity[]|null
*/
private $logos;
/**
- * @var array
+ * @var Entity[]|null
*/
private $labels;
/**
- * @var array
+ * @var Entity[]|null
*/
private $text;
/**
- * @var SafeSearch
+ * @var Document|null
+ */
+ private $fullText;
+
+ /**
+ * @var SafeSearch|null
*/
private $safeSearch;
/**
- * @var ImageProperties
+ * @var ImageProperties|null
*/
private $imageProperties;
/**
- * @var array
+ * @var CropHint[]|null
+ */
+ private $cropHints;
+
+ /**
+ * @var Web|null
+ */
+ private $web;
+
+ /**
+ * @var array|null
*/
private $error;
@@ -142,6 +159,10 @@ public function __construct($info)
}
}
+ if (isset($info['fullTextAnnotation'])) {
+ $this->fullText = new Document($info['fullTextAnnotation']);
+ }
+
if (isset($info['safeSearchAnnotation'])) {
$this->safeSearch = new SafeSearch($info['safeSearchAnnotation']);
}
@@ -150,6 +171,17 @@ public function __construct($info)
$this->imageProperties = new ImageProperties($info['imagePropertiesAnnotation']);
}
+ if (isset($info['cropHintsAnnotation']) && is_array($info['cropHintsAnnotation']['cropHints'])) {
+ $this->cropHints = [];
+ foreach ($info['cropHintsAnnotation']['cropHints'] as $hint) {
+ $this->cropHints[] = new CropHint($hint);
+ }
+ }
+
+ if (isset($info['webDetection'])) {
+ $this->web = new Web($info['webDetection']);
+ }
+
if (isset($info['error'])) {
$this->error = $info['error'];
}
@@ -182,6 +214,8 @@ public function info()
* $faces = $annotation->faces();
* ```
*
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#FaceAnnotation FaceAnnotation
+ *
* @return Face[]|null
*/
public function faces()
@@ -197,6 +231,8 @@ public function faces()
* $landmarks = $annotation->landmarks();
* ```
*
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#EntityAnnotation EntityAnnotation
+ *
* @return Entity[]|null
*/
public function landmarks()
@@ -212,6 +248,8 @@ public function landmarks()
* $logos = $annotation->logos();
* ```
*
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#EntityAnnotation EntityAnnotation
+ *
* @return Entity[]|null
*/
public function logos()
@@ -227,6 +265,8 @@ public function logos()
* $labels = $annotation->labels();
* ```
*
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#EntityAnnotation EntityAnnotation
+ *
* @return Entity[]|null
*/
public function labels()
@@ -242,6 +282,8 @@ public function labels()
* $text = $annotation->text();
* ```
*
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#EntityAnnotation EntityAnnotation
+ *
* @return Entity[]|null
*/
public function text()
@@ -249,6 +291,23 @@ public function text()
return $this->text;
}
+ /**
+ * Return the full text annotation.
+ *
+ * Example:
+ * ```
+ * $fullText = $annotation->fullText();
+ * ```
+ *
+ * @see https://cloud.google.com/vision/reference/rest/v1/images/annotate#fulltextannotation FullTextAnnotation
+ *
+ * @return Document|null
+ */
+ public function fullText()
+ {
+ return $this->fullText;
+ }
+
/**
* Get the result of a safe search detection
*
@@ -257,6 +316,10 @@ public function text()
* $safeSearch = $annotation->safeSearch();
* ```
*
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#SafeSearchAnnotation SafeSearchAnnotation
+ * @codingStandardsIgnoreEnd
+ *
* @return SafeSearch|null
*/
public function safeSearch()
@@ -272,6 +335,8 @@ public function safeSearch()
* $properties = $annotation->imageProperties();
* ```
*
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#ImageProperties ImageProperties
+ *
* @return ImageProperties|null
*/
public function imageProperties()
@@ -279,6 +344,42 @@ public function imageProperties()
return $this->imageProperties;
}
+ /**
+ * Fetch Crop Hints
+ *
+ * Example:
+ * ```
+ * $hints = $annotation->cropHints();
+ * ```
+ *
+ * @codingStandardsIgnoreStart
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#CropHintsAnnotation CropHintsAnnotation
+ * @codingStandardsIgnoreEnd
+ *
+ * @return CropHint[]|null
+ */
+ public function cropHints()
+ {
+ return $this->cropHints;
+ }
+
+ /**
+ * Fetch the Web Annotatation.
+ *
+ * Example:
+ * ```
+ * $web = $annotation->web();
+ * ```
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#WebDetection WebDetection
+ *
+ * @return Web|null
+ */
+ public function web()
+ {
+ return $this->web;
+ }
+
/**
* Get error information, if present
*
diff --git a/src/Vision/Annotation/CropHint.php b/src/Vision/Annotation/CropHint.php
new file mode 100644
index 000000000000..26f7f76d83e6
--- /dev/null
+++ b/src/Vision/Annotation/CropHint.php
@@ -0,0 +1,84 @@
+image($imageResource, [ 'CROP_HINTS' ]);
+ * $annotation = $vision->annotate($image);
+ *
+ * $hints = $annotation->cropHints();
+ * $hint = $hints[0];
+ * ```
+ *
+ * @method boundingPoly() {
+ * The bounding polygon of the recommended crop.
+ *
+ * Example:
+ * ```
+ * $poly = $hint->boundingPoly();
+ * ```
+ *
+ * @return array [BoundingPoly](https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#boundingpoly)
+ * }
+ * @method confidence() {
+ * Confidence of this being a salient region. Range [0, 1].
+ *
+ * Example:
+ * ```
+ * $confidence = $hint->confidence();
+ * ```
+ *
+ * @return float
+ * }
+ * @method importanceFraction() {
+ * Fraction of importance of this salient region with respect to the
+ * original image.
+ *
+ * Example:
+ * ```
+ * $importance = $hint->importanceFraction();
+ * ```
+ *
+ * @return float
+ * }
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#CropHint CropHint
+ */
+class CropHint extends AbstractFeature
+{
+ use CallTrait;
+
+ /**
+ * @param array $info Crop Hint result
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+ }
+}
diff --git a/src/Vision/Annotation/Document.php b/src/Vision/Annotation/Document.php
new file mode 100644
index 000000000000..e303c1a713c4
--- /dev/null
+++ b/src/Vision/Annotation/Document.php
@@ -0,0 +1,82 @@
+image($imageResource, [ 'DOCUMENT_TEXT_DETECTION' ]);
+ * $annotation = $vision->annotate($image);
+ *
+ * $document = $annotation->fullText();
+ * ```
+ *
+ * @method pages() {
+ * Get the document pages.
+ *
+ * Example:
+ * ```
+ * $pages = $document->pages();
+ * ```
+ *
+ * @return array
+ * }
+ * @method text() {
+ * Get the document text.
+ *
+ * Example:
+ * ```
+ * $text = $document->text();
+ * ```
+ *
+ * @return string
+ * }
+ * @method info() {
+ * Get the Document Text detection result.
+ *
+ * Example:
+ * ```
+ * $info = $document->info();
+ * ```
+ *
+ * @return array
+ * }
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#TextAnnotation TextAnnotation
+ */
+class Document extends AbstractFeature
+{
+ use CallTrait;
+
+ /**
+ * @param array $info Document Text Annotation response.
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+ }
+}
diff --git a/src/Vision/Annotation/Entity.php b/src/Vision/Annotation/Entity.php
index a7a988a3046a..042615cf9cac 100644
--- a/src/Vision/Annotation/Entity.php
+++ b/src/Vision/Annotation/Entity.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Vision\Annotation;
-use Google\Cloud\CallTrait;
+use Google\Cloud\Core\CallTrait;
/**
* Represents an entity annotation. Entities are created by several
@@ -26,12 +26,11 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Vision\VisionClient;
*
- * $cloud = new ServiceBuilder();
- * $vision = $cloud->vision();
+ * $vision = new VisionClient();
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = $vision->image($imageResource, [ 'text' ]);
* $annotation = $vision->annotate($image);
*
diff --git a/src/Vision/Annotation/Face.php b/src/Vision/Annotation/Face.php
index eca4d39591a2..839fd1744bb4 100644
--- a/src/Vision/Annotation/Face.php
+++ b/src/Vision/Annotation/Face.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Vision\Annotation;
-use Google\Cloud\CallTrait;
+use Google\Cloud\Core\CallTrait;
use Google\Cloud\Vision\Annotation\Face\Landmarks;
/**
@@ -25,12 +25,11 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Vision\VisionClient;
*
- * $cloud = new ServiceBuilder();
- * $vision = $cloud->vision();
+ * $vision = new VisionClient();
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = $vision->image($imageResource, [ 'FACE_DETECTION' ]);
* $annotation = $vision->annotate($image);
*
diff --git a/src/Vision/Annotation/Face/Landmarks.php b/src/Vision/Annotation/Face/Landmarks.php
index b0688ef05ff5..85721ce35103 100644
--- a/src/Vision/Annotation/Face/Landmarks.php
+++ b/src/Vision/Annotation/Face/Landmarks.php
@@ -24,12 +24,11 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Vision\VisionClient;
*
- * $cloud = new ServiceBuilder();
- * $vision = $cloud->vision();
+ * $vision = new VisionClient();
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = $vision->image($imageResource, ['FACE_DETECTION']);
* $annotation = $vision->annotate($image);
*
diff --git a/src/Vision/Annotation/ImageProperties.php b/src/Vision/Annotation/ImageProperties.php
index e936b6a75d3a..5d6e8c17283c 100644
--- a/src/Vision/Annotation/ImageProperties.php
+++ b/src/Vision/Annotation/ImageProperties.php
@@ -22,12 +22,11 @@
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Vision\VisionClient;
*
- * $cloud = new ServiceBuilder();
- * $vision = $cloud->vision();
+ * $vision = new VisionClient();
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = $vision->image($imageResource, [ 'imageProperties' ]);
* $annotation = $vision->annotate($image);
*
@@ -44,6 +43,8 @@
*
* @return array
* }
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#ImageProperties ImageProperties
*/
class ImageProperties extends AbstractFeature
{
diff --git a/src/Vision/Annotation/SafeSearch.php b/src/Vision/Annotation/SafeSearch.php
index 46d34f86bcf4..5e51ab19af5d 100644
--- a/src/Vision/Annotation/SafeSearch.php
+++ b/src/Vision/Annotation/SafeSearch.php
@@ -17,19 +17,18 @@
namespace Google\Cloud\Vision\Annotation;
-use Google\Cloud\CallTrait;
+use Google\Cloud\Core\CallTrait;
/**
* Represents a SafeSearch annotation result
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Vision\VisionClient;
*
- * $cloud = new ServiceBuilder();
- * $vision = $cloud->vision();
+ * $vision = new VisionClient();
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = $vision->image($imageResource, [ 'safeSearch' ]);
* $annotation = $vision->annotate($image);
*
@@ -87,6 +86,8 @@
*
* @return array
* }
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#SafeSearchAnnotation SafeSearchAnnotation
*/
class SafeSearch extends AbstractFeature
{
diff --git a/src/Vision/Annotation/Web.php b/src/Vision/Annotation/Web.php
new file mode 100644
index 000000000000..a19c1f137c0b
--- /dev/null
+++ b/src/Vision/Annotation/Web.php
@@ -0,0 +1,171 @@
+image($imageResource, [ 'WEB_DETECTION' ]);
+ * $annotation = $vision->annotate($image);
+ *
+ * $web = $annotation->web();
+ * ```
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#WebDetection WebDetection
+ */
+class Web extends AbstractFeature
+{
+ /**
+ * @var WebEntity[]
+ */
+ private $entities;
+
+ /**
+ * @var WebImage[]|null
+ */
+ private $matchingImages;
+
+ /**
+ * @var WebImage[]|null
+ */
+ private $partialMatchingImages;
+
+ /**
+ * @var WebPage[]|null
+ */
+ private $pages;
+
+ /**
+ * Create a Web result.
+ *
+ * @param array $info The annotation result
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+
+ if (isset($info['webEntities'])) {
+ $this->entities = [];
+
+ foreach ($info['webEntities'] as $entity) {
+ $this->entities[] = new WebEntity($entity);
+ }
+ }
+
+ if (isset($info['fullMatchingImages'])) {
+ $this->matchingImages = [];
+
+ foreach ($info['fullMatchingImages'] as $image) {
+ $this->matchingImages[] = new WebImage($image);
+ }
+ }
+
+ if (isset($info['partialMatchingImages'])) {
+ $this->partialMatchingImages = [];
+
+ foreach ($info['partialMatchingImages'] as $image) {
+ $this->partialMatchingImages[] = new WebImage($image);
+ }
+ }
+
+ if (isset($info['pagesWithMatchingImages'])) {
+ $this->pages = [];
+
+ foreach ($info['pagesWithMatchingImages'] as $page) {
+ $this->pages[] = new WebPage($page);
+ }
+ }
+ }
+
+ /**
+ * Entities deduced from similar images on the Internet.
+ *
+ * Example:
+ * ```
+ * $entities = $web->entities();
+ * ```
+ *
+ * @return WebEntity[]|null
+ */
+ public function entities()
+ {
+ return $this->entities;
+ }
+
+ /**
+ * Fully matching images from the internet.
+ *
+ * Images are most likely near duplicates, and most often are a copy of the
+ * given query image with a size change.
+ *
+ * Example:
+ * ```
+ * $images = $web->matchingImages();
+ * ```
+ *
+ * @return WebImage[]|null
+ */
+ public function matchingImages()
+ {
+ return $this->matchingImages;
+ }
+
+ /**
+ * Partial matching images from the Internet.
+ *
+ * Those images are similar enough to share some key-point features. For
+ * example an original image will likely have partial matching for its crops.
+ *
+ * Example:
+ * ```
+ * $images = $web->partialMatchingImages();
+ * ```
+ *
+ * @return WebImage[]|null
+ */
+ public function partialMatchingImages()
+ {
+ return $this->partialMatchingImages;
+ }
+
+ /**
+ * Web pages containing the matching images from the Internet.
+ *
+ * Example:
+ * ```
+ * $pages = $web->pages();
+ * ```
+ *
+ * @return WebPage[]|null
+ */
+ public function pages()
+ {
+ return $this->pages;
+ }
+}
diff --git a/src/Vision/Annotation/Web/WebEntity.php b/src/Vision/Annotation/Web/WebEntity.php
new file mode 100644
index 000000000000..98d9c770f204
--- /dev/null
+++ b/src/Vision/Annotation/Web/WebEntity.php
@@ -0,0 +1,86 @@
+image($imageResource, ['WEB_DETECTION']);
+ * $annotation = $vision->annotate($image);
+ *
+ * $entities = $annotation->web()->entities();
+ * $firstEntity = $entities[0];
+ * ```
+ *
+ * @method entityId() {
+ * The Entity ID
+ *
+ * Example:
+ * ```
+ * $id = $entity->entityId();
+ * ```
+ *
+ * @return string
+ * }
+ * @method score() {
+ * Overall relevancy score for the image.
+ *
+ * Not normalized and not comparable across different image queries.
+ *
+ * Example:
+ * ```
+ * $score = $entity->score();
+ * ```
+ *
+ * @return float
+ * }
+ * @method description() {
+ * Canonical description of the entity, in English.
+ *
+ * Example:
+ * ```
+ * $description = $entity->description();
+ * ```
+ *
+ * @return string
+ * }
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#WebEntity WebEntity
+ */
+class WebEntity extends AbstractFeature
+{
+ use CallTrait;
+
+ /**
+ * @param array $info WebEntity info
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+ }
+}
diff --git a/src/Vision/Annotation/Web/WebImage.php b/src/Vision/Annotation/Web/WebImage.php
new file mode 100644
index 000000000000..4482fd717cce
--- /dev/null
+++ b/src/Vision/Annotation/Web/WebImage.php
@@ -0,0 +1,76 @@
+image($imageResource, ['WEB_DETECTION']);
+ * $annotation = $vision->annotate($image);
+ *
+ * $matchingImages = $annotation->web()->matchingImages();
+ * $firstImage = $matchingImages[0];
+ * ```
+ *
+ * @method url() {
+ * The result image URL
+ *
+ * Example:
+ * ```
+ * $url = $image->url();
+ * ```
+ *
+ * @return string
+ * }
+ * @method score() {
+ * Overall relevancy score for the image.
+ *
+ * Not normalized and not comparable across different image queries.
+ *
+ * Example:
+ * ```
+ * $score = $image->score();
+ * ```
+ *
+ * @return float
+ * }
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#WebImage WebImage
+ */
+class WebImage extends AbstractFeature
+{
+ use CallTrait;
+
+ /**
+ * @param array $info The WebImage result
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+ }
+}
diff --git a/src/Vision/Annotation/Web/WebPage.php b/src/Vision/Annotation/Web/WebPage.php
new file mode 100644
index 000000000000..262c7565a853
--- /dev/null
+++ b/src/Vision/Annotation/Web/WebPage.php
@@ -0,0 +1,76 @@
+image($imageResource, ['WEB_DETECTION']);
+ * $annotation = $vision->annotate($image);
+ *
+ * $pages = $annotation->web()->pages();
+ * $firstPage = $pages[0];
+ * ```
+ *
+ * @method url() {
+ * The result web page URL
+ *
+ * Example:
+ * ```
+ * $url = $image->url();
+ * ```
+ *
+ * @return string
+ * }
+ * @method score() {
+ * Overall relevancy score for the image.
+ *
+ * Not normalized and not comparable across different image queries.
+ *
+ * Example:
+ * ```
+ * $score = $image->score();
+ * ```
+ *
+ * @return float
+ * }
+ *
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#WebPage WebPage
+ */
+class WebPage extends AbstractFeature
+{
+ use CallTrait;
+
+ /**
+ * @param array $info The WebPage result
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+ }
+}
diff --git a/src/Vision/Connection/Rest.php b/src/Vision/Connection/Rest.php
index e083b1b5497a..36ebedd47c19 100644
--- a/src/Vision/Connection/Rest.php
+++ b/src/Vision/Connection/Rest.php
@@ -17,10 +17,11 @@
namespace Google\Cloud\Vision\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
-use Google\Cloud\RestTrait;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
+use Google\Cloud\Core\UriTrait;
+use Google\Cloud\Vision\VisionClient;
/**
* Implementation of the
@@ -39,7 +40,8 @@ class Rest implements ConnectionInterface
public function __construct(array $config = [])
{
$config += [
- 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/vision-v1.json'
+ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/vision-v1.json',
+ 'componentVersion' => VisionClient::VERSION
];
$this->setRequestWrapper(new RequestWrapper($config));
diff --git a/src/Vision/Connection/ServiceDefinition/vision-v1.json b/src/Vision/Connection/ServiceDefinition/vision-v1.json
index 329a854b0671..2bcdadacfcae 100644
--- a/src/Vision/Connection/ServiceDefinition/vision-v1.json
+++ b/src/Vision/Connection/ServiceDefinition/vision-v1.json
@@ -44,6 +44,10 @@
"gcsImageUri": {
"description": "Google Cloud Storage image URI. It must be in the following form:\n`gs://bucket_name/object_name`. For more\ndetails, please see: https://cloud.google.com/storage/docs/reference-uris.\nNOTE: Cloud Storage object versioning is not supported!",
"type": "string"
+ },
+ "ImageUri": {
+ "description": "Image URI which supports: 1) Google Cloud Storage image URI, which must be in the following form: `gs://bucket_name/object_name` (for details, see [Google Cloud Storage Request URIs](https://cloud.google.com/storage/docs/reference-uris)). NOTE: Cloud Storage object versioning is not supported. 2) Publicly accessible image HTTP/HTTPS URL. This is preferred over the legacy `gcs_image_uri` above. When both `gcs_image_uri` and `image_uri` are specified, `image_uri` takes precedence.",
+ "type": "string"
}
},
"id": "ImageSource"
@@ -761,8 +765,11 @@
"LOGO_DETECTION",
"LABEL_DETECTION",
"TEXT_DETECTION",
+ "DOCUMENT_TEXT_DETECTION",
"SAFE_SEARCH_DETECTION",
- "IMAGE_PROPERTIES"
+ "IMAGE_PROPERTIES",
+ "CROP_HINTS",
+ "WEB_DETECTION"
],
"enumDescriptions": [
"Unspecified feature type.",
@@ -771,8 +778,11 @@
"Run logo detection.",
"Run label detection.",
"Run OCR.",
+ "Run dense text document OCR. Takes precedence when both DOCUMENT_TEXT_DETECTION and TEXT_DETECTION are present.",
"Run various computer vision models to compute image safe-search properties.",
- "Compute a set of properties about the image (such as the image's dominant colors)."
+ "Compute a set of properties about the image (such as the image's dominant colors).",
+ "Run crop hints.",
+ "Run web annotation."
],
"type": "string"
},
diff --git a/src/Vision/Image.php b/src/Vision/Image.php
index 50e8f2e85404..3556821baff3 100644
--- a/src/Vision/Image.php
+++ b/src/Vision/Image.php
@@ -36,18 +36,17 @@
* combined size of all images in a request. Reducing your file size can
* significantly improve throughput; however, be careful not to reduce image
* quality in the process. See
- * [Best Practices - Image Sizing](https://cloud.google.com/vision/docs/image-best-practices#image_sizing)
+ * [Best Practices - Image Sizing](https://cloud.google.com/vision/docs/best-practices#image_sizing)
* for current file size limits.
*
* Example:
* ```
* //[snippet=default]
- * use Google\Cloud\ServiceBuilder;
+ * use Google\Cloud\Vision\VisionClient;
*
- * $cloud = new ServiceBuilder();
- * $vision = $cloud->vision();
+ * $vision = new VisionClient();
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = $vision->image($imageResource, [
* 'FACE_DETECTION'
* ]);
@@ -58,7 +57,7 @@
* // Images can be directly instantiated.
* use Google\Cloud\Vision\Image;
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = new Image($imageResource, [
* 'FACE_DETECTION'
* ]);
@@ -79,10 +78,11 @@
* ```
* //[snippet=gcs]
* // Files stored in Google Cloud Storage can be used.
- *
+ * use Google\Cloud\Storage\StorageClient;
* use Google\Cloud\Vision\Image;
*
- * $file = $cloud->storage()->bucket('my-test-bucket')->object('family-photo.jpg');
+ * $storage = new StorageClient();
+ * $file = $storage->bucket('my-test-bucket')->object('family-photo.jpg');
* $image = new Image($file, [
* 'FACE_DETECTION'
* ]);
@@ -94,7 +94,7 @@
*
* use Google\Cloud\Vision\Image;
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = new Image($imageResource, [
* 'FACE_DETECTION',
* 'LOGO_DETECTION'
@@ -123,26 +123,29 @@
*
* use Google\Cloud\Vision\Image;
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = new Image($imageResource, [
* 'faces', // Corresponds to `FACE_DETECTION`
* 'landmarks', // Corresponds to `LANDMARK_DETECTION`
* 'logos', // Corresponds to `LOGO_DETECTION`
* 'labels', // Corresponds to `LABEL_DETECTION`
- * 'text', // Corresponds to `TEXT_DETECTION`
+ * 'text', // Corresponds to `TEXT_DETECTION`,
+ * 'document', // Corresponds to `DOCUMENT_TEXT_DETECTION`
* 'safeSearch', // Corresponds to `SAFE_SEARCH_DETECTION`
- * 'imageProperties' // Corresponds to `IMAGE_PROPERTIES`
+ * 'imageProperties',// Corresponds to `IMAGE_PROPERTIES`
+ * 'crop', // Corresponds to `CROP_HINTS`
+ * 'web' // Corresponds to `WEB_DETECTION`
* ]);
* ```
*
- * @see https://cloud.google.com/vision/docs/image-best-practices Best Practices
+ * @see https://cloud.google.com/vision/docs/best-practices Best Practices
* @see https://cloud.google.com/vision/docs/pricing Pricing
*/
class Image
{
const TYPE_BYTES = 'bytes';
- const TYPE_STORAGE = 'storage';
const TYPE_STRING = 'string';
+ const TYPE_URI = 'uri';
/**
* @var mixed
@@ -166,16 +169,31 @@ class Image
/**
* A map of short names to identifiers recognized by Cloud Vision.
+ *
* @var array
*/
private $featureShortNames = [
- 'faces' => 'FACE_DETECTION',
- 'landmarks' => 'LANDMARK_DETECTION',
- 'logos' => 'LOGO_DETECTION',
- 'labels' => 'LABEL_DETECTION',
- 'text' => 'TEXT_DETECTION',
- 'safeSearch' => 'SAFE_SEARCH_DETECTION',
- 'imageProperties' => 'IMAGE_PROPERTIES'
+ 'faces' => 'FACE_DETECTION',
+ 'landmarks' => 'LANDMARK_DETECTION',
+ 'logos' => 'LOGO_DETECTION',
+ 'labels' => 'LABEL_DETECTION',
+ 'text' => 'TEXT_DETECTION',
+ 'document' => 'DOCUMENT_TEXT_DETECTION',
+ 'safeSearch' => 'SAFE_SEARCH_DETECTION',
+ 'imageProperties' => 'IMAGE_PROPERTIES',
+ 'crop' => 'CROP_HINTS',
+ 'web' => 'WEB_DETECTION'
+ ];
+
+ /**
+ * A list of allowed url schemes.
+ *
+ * @var array
+ */
+ private $urlSchemes = [
+ 'http',
+ 'https',
+ 'gs'
];
/**
@@ -183,15 +201,15 @@ class Image
*
* @param resource|string|StorageObject $image An image to configure with
* the given settings. This parameter will accept a resource, a
- * string of bytes, or an instance of
- * {@see Google\Cloud\Storage\StorageObject}.
+ * string of bytes, the URI of an image in a publicly-accessible
+ * web location, or an instance of {@see Google\Cloud\Storage\StorageObject}.
* @param array $features A list of cloud vision
* [features](https://cloud.google.com/vision/reference/rest/v1/images/annotate#type)
* to apply to the image. Google Cloud Platform Client Library provides a set of abbreviated
* names which can be used in the interest of brevity in place of
* the names offered by the cloud vision service. These names are
- * `faces`, `landmarks`, `logos`, `labels`, `text`, `safeSearch`
- * and `imageProperties`.
+ * `faces`, `landmarks`, `logos`, `labels`, `text`, `document`,
+ * `safeSearch`, `imageProperties`, `crop`, and `web`.
* @param array $options {
* Configuration Options
*
@@ -218,22 +236,21 @@ public function __construct($image, array $features, array $options = [])
$this->features = $this->normalizeFeatures($features);
- if ($image instanceof StorageObject) {
- $identity = $image->identity();
- $uri = sprintf('gs://%s/%s', $identity['bucket'], $identity['object']);
-
- $this->type = self::TYPE_STORAGE;
- $this->image = $uri;
+ $this->image = $image;
+ if (is_string($image) && in_array(parse_url($image, PHP_URL_SCHEME), $this->urlSchemes)) {
+ $this->type = self::TYPE_URI;
} elseif (is_string($image)) {
$this->type = self::TYPE_STRING;
- $this->image = $image;
+ } elseif ($image instanceof StorageObject) {
+ $this->type = self::TYPE_URI;
+ $this->image = $image->gcsUri();
} elseif (is_resource($image)) {
$this->type = self::TYPE_BYTES;
$this->image = Psr7\stream_for($image);
} else {
throw new InvalidArgumentException(
'Given image is not valid. ' .
- 'Image must be a string of bytes, a google storage object, or a resource.'
+ 'Image must be a string of bytes, a google storage object, a valid image URI, or a resource.'
);
}
}
@@ -248,7 +265,7 @@ public function __construct($image, array $features, array $options = [])
* ```
* use Google\Cloud\Vision\Image;
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
* $image = new Image($imageResource, [
* 'FACE_DETECTION'
* ]);
@@ -302,7 +319,7 @@ private function imageObject($encode)
return [
'source' => [
- 'gcsImageUri' => $this->image
+ 'imageUri' => $this->image
]
];
}
diff --git a/src/Vision/LICENSE b/src/Vision/LICENSE
new file mode 100644
index 000000000000..8f71f43fee3f
--- /dev/null
+++ b/src/Vision/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/src/Vision/README.md b/src/Vision/README.md
new file mode 100644
index 000000000000..0ad0b3235379
--- /dev/null
+++ b/src/Vision/README.md
@@ -0,0 +1,16 @@
+# Google Cloud PHP Vision
+
+> Idiomatic PHP client for [Cloud Vision](https://cloud.google.com/vision/).
+
+* [Homepage](http://googlecloudplatform.github.io/google-cloud-php)
+* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-vision/latest/vision/visionclient)
+
+**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any
+support requests, bug reports, or development contributions should be directed to
+that project.
+
+## Installation
+
+```
+$ composer require google/cloud-vision
+```
diff --git a/src/Vision/VERSION b/src/Vision/VERSION
new file mode 100644
index 000000000000..6c6aa7cb0918
--- /dev/null
+++ b/src/Vision/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/src/Vision/VisionClient.php b/src/Vision/VisionClient.php
index ae3dd47bba7a..19f2bd1cbbcd 100644
--- a/src/Vision/VisionClient.php
+++ b/src/Vision/VisionClient.php
@@ -17,29 +17,21 @@
namespace Google\Cloud\Vision;
-use Google\Cloud\ClientTrait;
-use Google\Cloud\ValidateTrait;
+use Google\Cloud\Core\ClientTrait;
+use Google\Cloud\Core\ValidateTrait;
+use Google\Cloud\Storage\StorageObject;
use Google\Cloud\Vision\Connection\Rest;
use InvalidArgumentException;
use Psr\Cache\CacheItemPoolInterface;
/**
- * Google Cloud Vision client. Allows you to understand the content of an image,
+ * Google Cloud Vision allows you to understand the content of an image,
* classify images into categories, detect text, objects, faces and more. Find
- * more information at
+ * more information at the
* [Google Cloud Vision docs](https://cloud.google.com/vision/docs/).
*
* Example:
* ```
- * use Google\Cloud\ServiceBuilder;
- *
- * $cloud = new ServiceBuilder();
- *
- * $vision = $cloud->vision();
- * ```
- *
- * ```
- * // VisionClient can be instantiated directly.
* use Google\Cloud\Vision\VisionClient;
*
* $vision = new VisionClient();
@@ -50,6 +42,8 @@ class VisionClient
use ClientTrait;
use ValidateTrait;
+ const VERSION = '0.1.0';
+
const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
/**
@@ -110,7 +104,7 @@ public function __construct(array $config = [])
*
* Example:
* ```
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
*
* $image = $vision->image($imageResource, [
* 'FACE_DETECTION'
@@ -120,7 +114,7 @@ public function __construct(array $config = [])
* ```
* // Setting maxResults for a feature
*
- * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
*
* $image = $vision->image($imageResource, [
* 'FACE_DETECTION'
@@ -133,8 +127,8 @@ public function __construct(array $config = [])
*
* @param resource|string|StorageObject $image An image to configure with
* the given settings. This parameter will accept a resource, a
- * string of bytes, or an instance of
- * {@see Google\Cloud\Storage\StorageObject}.
+ * string of bytes, the URI of an image in a publicly-accessible
+ * web location, or an instance of {@see Google\Cloud\Storage\StorageObject}.
* @param array $features A list of cloud vision
* [features](https://cloud.google.com/vision/reference/rest/v1/images/annotate#type)
* to apply to the image.
@@ -169,8 +163,8 @@ public function image($image, array $features, array $options = [])
* // In the example below, both images will have the same settings applied.
* // They will both run face detection and return up to 10 results.
*
- * $familyPhotoResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
- * $weddingPhotoResource = fopen(__DIR__ .'/assets/wedding-photo.jpg', 'r');
+ * $familyPhotoResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
+ * $weddingPhotoResource = fopen(__DIR__ . '/assets/wedding-photo.jpg', 'r');
*
* $images = $vision->images([$familyPhotoResource, $weddingPhotoResource], [
* 'FACE_DETECTION'
@@ -183,7 +177,8 @@ public function image($image, array $features, array $options = [])
*
* @param resource[]|string[]|StorageObject[] $images An array of images
* to configure with the given settings. Each member of the set can
- * be a resource, a string of bytes, or an instance of
+ * be a resource, a string of bytes, the URI of an image in a
+ * publicly-accessible web location, or an instance of
* {@see Google\Cloud\Storage\StorageObject}.
* @param array $features A list of cloud vision features to apply to each image.
* @param array $options See {@see Google\Cloud\Vision\Image::__construct()} for
@@ -206,7 +201,7 @@ public function images(array $images, array $features, array $options = [])
*
* Example:
* ```
- * $familyPhotoResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
+ * $familyPhotoResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
*
* $image = $vision->image($familyPhotoResource, [
* 'FACE_DETECTION'
@@ -215,6 +210,8 @@ public function images(array $images, array $features, array $options = [])
* $result = $vision->annotate($image);
* ```
*
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate Annotate API documentation
+ *
* @param Image $image The image to annotate
* @param array $options Configuration options
* @return Annotation
@@ -232,8 +229,8 @@ public function annotate(Image $image, array $options = [])
* ```
* $images = [];
*
- * $familyPhotoResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r');
- * $eiffelTowerResource = fopen(__DIR__ .'/assets/eiffel-tower.jpg', 'r');
+ * $familyPhotoResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r');
+ * $eiffelTowerResource = fopen(__DIR__ . '/assets/eiffel-tower.jpg', 'r');
*
* $images[] = $vision->image($familyPhotoResource, [
* 'FACE_DETECTION'
@@ -246,6 +243,8 @@ public function annotate(Image $image, array $options = [])
* $result = $vision->annotateBatch($images);
* ```
*
+ * @see https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate Annotate API documentation
+ *
* @param Image[] $images An array consisting of instances of
* {@see Google\Cloud\Vision\Image}.
* @param array $options Configuration Options
diff --git a/src/Vision/composer.json b/src/Vision/composer.json
new file mode 100644
index 000000000000..655613b3a174
--- /dev/null
+++ b/src/Vision/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "google/cloud-vision",
+ "description": "Cloud Vision Client for PHP",
+ "license": "Apache-2.0",
+ "minimum-stability": "stable",
+ "require": {
+ "google/cloud-core": "*"
+ },
+ "suggest": {
+ "google/cloud-storage": "Annotate images stored in Google Cloud Storage"
+ },
+ "extra": {
+ "component": {
+ "id": "cloud-vision",
+ "target": "GoogleCloudPlatform/google-cloud-php-vision.git",
+ "path": "src/Vision",
+ "entry": "VisionClient.php"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Google\\Cloud\\Vision\\": ""
+ }
+ }
+}
diff --git a/tests/snippets/BigQuery/BigQueryClientTest.php b/tests/snippets/BigQuery/BigQueryClientTest.php
index 2b02c17011de..b6527511ef23 100644
--- a/tests/snippets/BigQuery/BigQueryClientTest.php
+++ b/tests/snippets/BigQuery/BigQueryClientTest.php
@@ -26,8 +26,9 @@
use Google\Cloud\BigQuery\Dataset;
use Google\Cloud\BigQuery\Job;
use Google\Cloud\BigQuery\QueryResults;
+use Google\Cloud\Core\Int64;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
-use Google\Cloud\Int64;
use Prophecy\Argument;
/**
@@ -62,8 +63,8 @@ class BigQueryClientTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->client = new \BigQueryClientStub;
- $this->client->setConnection($this->connection->reveal());
+ $this->client = \Google\Cloud\Dev\stub(BigQueryClient::class);
+ $this->client->___setProperty('connection', $this->connection->reveal());
}
public function testClass()
@@ -74,14 +75,6 @@ public function testClass()
$this->assertInstanceOf(BigQueryClient::class, $res->returnVal());
}
- public function testClassDirectInstantiation()
- {
- $snippet = $this->snippetFromClass(BigQueryClient::class, 1);
- $res = $snippet->invoke('bigQuery');
-
- $this->assertInstanceOf(BigQueryClient::class, $res->returnVal());
- }
-
public function testRunQuery()
{
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'runQuery');
@@ -98,7 +91,7 @@ public function testRunQuery()
$this->connection->getQueryResults(Argument::any())
->shouldBeCalledTimes(1)
->willReturn($this->result);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('queryResults');
$this->assertInstanceOf(QueryResults::class, $res->returnVal());
@@ -141,7 +134,7 @@ public function testRunQueryWithNamedParameters()
$this->connection->getQueryResults(Argument::any())
->shouldBeCalledTimes(1)
->willReturn($this->result);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('queryResults');
$this->assertInstanceOf(QueryResults::class, $res->returnVal());
@@ -174,7 +167,7 @@ public function testRunQueryWithPositionalParameters()
$this->connection->getQueryResults(Argument::any())
->shouldBeCalledTimes(1)
->willReturn($this->result);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('queryResults');
$this->assertInstanceOf(QueryResults::class, $res->returnVal());
@@ -205,7 +198,7 @@ public function testRunQueryAsJob()
],
$this->result
);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('queryResults');
$this->assertInstanceOf(QueryResults::class, $res->returnVal());
@@ -216,7 +209,7 @@ public function testJob()
{
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'job');
$snippet->addLocal('bigQuery', $this->client);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('job');
$this->assertInstanceOf(Job::class, $res->returnVal());
@@ -237,10 +230,10 @@ public function testJobs()
]
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('jobs');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('job', trim($res->output()));
}
@@ -248,7 +241,7 @@ public function testDataset()
{
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'dataset');
$snippet->addLocal('bigQuery', $this->client);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('dataset');
$this->assertInstanceOf(Dataset::class, $res->returnVal());
@@ -269,10 +262,10 @@ public function testDatasets()
]
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('datasets');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('dataset', trim($res->output()));
}
@@ -283,7 +276,7 @@ public function testCreateDataset()
$this->connection->insertDataset(Argument::any())
->shouldBeCalledTimes(1)
->willReturn([]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('dataset');
$this->assertInstanceOf(Dataset::class, $res->returnVal());
@@ -293,7 +286,7 @@ public function testBytes()
{
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'bytes');
$snippet->addLocal('bigQuery', $this->client);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('bytes');
$this->assertInstanceOf(Bytes::class, $res->returnVal());
@@ -303,7 +296,7 @@ public function testDate()
{
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'date');
$snippet->addLocal('bigQuery', $this->client);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('date');
$this->assertInstanceOf(Date::class, $res->returnVal());
@@ -313,7 +306,7 @@ public function testInt64()
{
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'int64');
$snippet->addLocal('bigQuery', $this->client);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('int64');
$this->assertInstanceOf(Int64::class, $res->returnVal());
@@ -323,7 +316,7 @@ public function testTime()
{
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'time');
$snippet->addLocal('bigQuery', $this->client);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('time');
$this->assertInstanceOf(Time::class, $res->returnVal());
@@ -333,7 +326,7 @@ public function testTimestamp()
{
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'timestamp');
$snippet->addLocal('bigQuery', $this->client);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('timestamp');
$this->assertInstanceOf(Timestamp::class, $res->returnVal());
diff --git a/tests/snippets/BigQuery/DatasetTest.php b/tests/snippets/BigQuery/DatasetTest.php
index e72271cb9580..7bcfc8b7c020 100644
--- a/tests/snippets/BigQuery/DatasetTest.php
+++ b/tests/snippets/BigQuery/DatasetTest.php
@@ -20,6 +20,8 @@
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
use Google\Cloud\BigQuery\Dataset;
use Google\Cloud\BigQuery\Table;
+use Google\Cloud\BigQuery\ValueMapper;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
use Prophecy\Argument;
@@ -30,9 +32,11 @@ class DatasetTest extends SnippetTestCase
{
private $identity;
private $connection;
+ private $mapper;
public function setUp()
{
+ $this->mapper = new ValueMapper(false);
$this->identity = ['datasetId' => 'id', 'projectId' => 'projectId'];
$this->connection = $this->prophesize(ConnectionInterface::class);
}
@@ -43,6 +47,7 @@ public function getDataset($connection, array $info = [])
$connection->reveal(),
$this->identity['datasetId'],
$this->identity['projectId'],
+ $this->mapper,
$info
);
}
@@ -112,7 +117,7 @@ public function testTables()
$snippet->addLocal('dataset', $dataset);
$res = $snippet->invoke('tables');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('table', trim($res->output()));
}
diff --git a/tests/snippets/BigQuery/JobTest.php b/tests/snippets/BigQuery/JobTest.php
index 7cb1ab1a7496..18f1f070174b 100644
--- a/tests/snippets/BigQuery/JobTest.php
+++ b/tests/snippets/BigQuery/JobTest.php
@@ -150,4 +150,22 @@ public function testReload()
$snippet->replace('sleep(1);', '');
$snippet->invoke();
}
+
+ public function testId()
+ {
+ $snippet = $this->snippetFromMethod(Job::class, 'id');
+ $snippet->addLocal('job', $this->getJob($this->connection, []));
+ $res = $snippet->invoke();
+
+ $this->assertEquals($res->output(), $this->identity['jobId']);
+ }
+
+ public function testIdentity()
+ {
+ $snippet = $this->snippetFromMethod(Job::class, 'identity');
+ $snippet->addLocal('job', $this->getJob($this->connection, []));
+ $res = $snippet->invoke();
+
+ $this->assertEquals($res->output(), $this->identity['projectId']);
+ }
}
diff --git a/tests/snippets/BigQuery/QueryResultsTest.php b/tests/snippets/BigQuery/QueryResultsTest.php
index cb4398fff155..058164489466 100644
--- a/tests/snippets/BigQuery/QueryResultsTest.php
+++ b/tests/snippets/BigQuery/QueryResultsTest.php
@@ -39,6 +39,7 @@ class QueryResultsTest extends SnippetTestCase
public function setUp()
{
$this->info = [
+ 'totalBytesProcessed' => 3,
'jobComplete' => false,
'jobReference' => [
'jobId' => 'job'
@@ -62,14 +63,14 @@ public function setUp()
$this->reload = [];
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->qr = new \QueryResultsStub(
+ $this->qr = \Google\Cloud\Dev\stub(QueryResults::class, [
$this->connection->reveal(),
self::JOB_ID,
self::PROJECT,
$this->info,
$this->reload,
new ValueMapper(false)
- );
+ ]);
}
public function testRows()
@@ -81,7 +82,7 @@ public function testRows()
$this->connection->getQueryResults(Argument::any())
->willReturn($this->info);
- $this->qr->setConnection($this->connection->reveal());
+ $this->qr->___setProperty('connection', $this->connection->reveal());
$this->qr->reload();
@@ -98,7 +99,7 @@ public function testIsComplete()
$this->connection->getQueryResults(Argument::any())
->willReturn($this->info);
- $this->qr->setConnection($this->connection->reveal());
+ $this->qr->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Query complete!', $res->output());
@@ -112,4 +113,29 @@ public function testIdentity()
$res = $snippet->invoke();
$this->assertEquals(self::PROJECT, $res->output());
}
+
+ public function testInfo()
+ {
+ $snippet = $this->snippetFromMethod(QueryResults::class, 'info');
+ $snippet->addLocal('queryResults', $this->qr);
+
+ $res = $snippet->invoke();
+ $this->assertEquals($this->info['totalBytesProcessed'], $res->output());
+ }
+
+ public function testReload()
+ {
+ $this->connection->getQueryResults(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['jobComplete' => true] + $this->info);
+
+ $this->qr->___setProperty('connection', $this->connection->reveal());
+
+ $snippet = $this->snippetFromMethod(QueryResults::class, 'reload');
+ $snippet->addLocal('queryResults', $this->qr);
+ $snippet->replace('sleep(1);', '');
+
+ $res = $snippet->invoke();
+ $this->assertEquals('Query complete!', $res->output());
+ }
}
diff --git a/tests/snippets/BigQuery/TableTest.php b/tests/snippets/BigQuery/TableTest.php
index a97c2978364d..088b1e3cf2c1 100644
--- a/tests/snippets/BigQuery/TableTest.php
+++ b/tests/snippets/BigQuery/TableTest.php
@@ -22,9 +22,12 @@
use Google\Cloud\BigQuery\InsertResponse;
use Google\Cloud\BigQuery\Job;
use Google\Cloud\BigQuery\Table;
+use Google\Cloud\BigQuery\ValueMapper;
+use Google\Cloud\Core\Iterator\ItemIterator;
+use Google\Cloud\Core\Upload\MultipartUploader;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
use Google\Cloud\Storage\Connection\ConnectionInterface as StorageConnectionInterface;
-use Google\Cloud\Upload\MultipartUploader;
+use Google\Cloud\Storage\StorageClient;
use Prophecy\Argument;
/**
@@ -39,6 +42,7 @@ class TableTest extends SnippetTestCase
private $info;
private $connection;
private $table;
+ private $mapper;
public function setUp()
{
@@ -61,14 +65,16 @@ public function setUp()
'friendlyName' => 'Jeffrey'
];
+ $this->mapper = new ValueMapper(false);
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->table = new \TableStub(
+ $this->table = \Google\Cloud\Dev\Stub(Table::class, [
$this->connection->reveal(),
self::ID,
self::DSID,
self::PROJECT,
+ $this->mapper,
$this->info
- );
+ ]);
}
public function testExists()
@@ -80,7 +86,7 @@ public function testExists()
->shouldBeCalled()
->willReturn([]);
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Table exists!', $res->output());
@@ -94,7 +100,7 @@ public function testDelete()
$this->connection->deleteTable(Argument::any())
->shouldBeCalled();
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -107,7 +113,7 @@ public function testUpdate()
$this->connection->patchTable(Argument::any())
->shouldBeCalled();
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -123,20 +129,20 @@ public function testRows()
'rows' => $this->info['rows']
]);
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('rows');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
- $this->assertEquals('abcd', $res->output());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
+ $this->assertEquals('abcd' . PHP_EOL, $res->output());
}
public function testCopy()
{
- $bq = new \BigQueryClientStub;
+ $bq = \Google\Cloud\Dev\stub(BigQueryClient::class);
$snippet = $this->snippetFromMethod(Table::class, 'copy');
$snippet->addLocal('bigQuery', $bq);
- $bq->setConnection($this->connection->reveal());
+ $bq->___setProperty('connection', $this->connection->reveal());
$this->connection->insertJob(Argument::any())
->shouldBeCalled()
@@ -146,7 +152,7 @@ public function testCopy()
]
]);
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('job');
$this->assertInstanceOf(Job::class, $res->returnVal());
@@ -154,8 +160,8 @@ public function testCopy()
public function testExport()
{
- $storage = new \StorageClientStub;
- $storage->setConnection($this->prophesize(StorageConnectionInterface::class)->reveal());
+ $storage = \Google\Cloud\Dev\stub(StorageClient::class);
+ $storage->___setProperty('connection', $this->prophesize(StorageConnectionInterface::class)->reveal());
$snippet = $this->snippetFromMethod(Table::class, 'export');
$snippet->addLocal('storage', $storage);
@@ -169,7 +175,7 @@ public function testExport()
]
]);
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('job');
$this->assertInstanceOf(Job::class, $res->returnVal());
@@ -194,7 +200,7 @@ public function testLoad()
->shouldBeCalled()
->willReturn($uploader->reveal());
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('job');
$this->assertInstanceOf(Job::class, $res->returnVal());
@@ -202,8 +208,8 @@ public function testLoad()
public function testLoadFromStorage()
{
- $storage = new \StorageClientStub;
- $storage->setConnection($this->prophesize(StorageConnectionInterface::class)->reveal());
+ $storage = \Google\Cloud\Dev\stub(StorageClient::class);
+ $storage->___setProperty('connection', $this->prophesize(StorageConnectionInterface::class)->reveal());
$snippet = $this->snippetFromMethod(Table::class, 'loadFromStorage');
$snippet->addLocal('storage', $storage);
@@ -222,7 +228,7 @@ public function testLoadFromStorage()
->shouldBeCalled()
->willReturn($uploader->reveal());
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('job');
$this->assertInstanceOf(Job::class, $res->returnVal());
@@ -239,7 +245,7 @@ public function testInsertRow()
]);
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('insertResponse');
$this->assertInstanceOf(InsertResponse::class, $res->returnVal());
@@ -256,7 +262,7 @@ public function testInsertRows()
]);
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('insertResponse');
$this->assertInstanceOf(InsertResponse::class, $res->returnVal());
@@ -282,7 +288,7 @@ public function testReload()
'friendlyName' => 'El Jefe'
]);
- $this->table->setConnection($this->connection->reveal());
+ $this->table->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('El Jefe', $res->output());
diff --git a/tests/snippets/BigQuery/TimeTest.php b/tests/snippets/BigQuery/TimeTest.php
index 9f782d703d81..0b3e072da53e 100644
--- a/tests/snippets/BigQuery/TimeTest.php
+++ b/tests/snippets/BigQuery/TimeTest.php
@@ -17,6 +17,7 @@
namespace Google\Cloud\Tests\Snippets\BigQuery;
+use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\BigQuery\Time;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
@@ -28,7 +29,7 @@ class TimeTest extends SnippetTestCase
public function testClass()
{
$snippet = $this->snippetFromClass(Time::class);
- $snippet->addLocal('bigQuery', new \BigQueryClientStub);
+ $snippet->addLocal('bigQuery', \Google\Cloud\Dev\stub(BigQueryClient::class));
$res = $snippet->invoke('time');
$this->assertInstanceOf(Time::class, $res->returnVal());
diff --git a/tests/snippets/BigQuery/TimestampTest.php b/tests/snippets/BigQuery/TimestampTest.php
index 9d79bfa88023..d732c8ce3cec 100644
--- a/tests/snippets/BigQuery/TimestampTest.php
+++ b/tests/snippets/BigQuery/TimestampTest.php
@@ -17,6 +17,7 @@
namespace Google\Cloud\Tests\Snippets\BigQuery;
+use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\BigQuery\Timestamp;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
@@ -28,7 +29,7 @@ class TimestampTest extends SnippetTestCase
public function testClass()
{
$snippet = $this->snippetFromClass(Timestamp::class);
- $snippet->addLocal('bigQuery', new \BigQueryClientStub);
+ $snippet->addLocal('bigQuery', \Google\Cloud\Dev\stub(BigQueryClient::class));
$res = $snippet->invoke('timestamp');
$this->assertInstanceOf(Timestamp::class, $res->returnVal());
diff --git a/tests/snippets/Core/Compute/MetadataTest.php b/tests/snippets/Core/Compute/MetadataTest.php
new file mode 100644
index 000000000000..9c64eb0099d4
--- /dev/null
+++ b/tests/snippets/Core/Compute/MetadataTest.php
@@ -0,0 +1,138 @@
+reader = $this->prophesize(ReaderInterface::class);
+ $this->metadata = new Metadata;
+ $this->metadata->setReader($this->reader->reveal());
+ }
+
+ public function testClass()
+ {
+ $this->reader->read('project/project-id')
+ ->shouldBeCalled()
+ ->willReturn(self::PROJECT);
+
+ $snippet = $this->snippetFromClass(Metadata::class);
+ $snippet->insertAfterLine(2, '$metadata->setReader($reader);');
+ $snippet->addLocal('reader', $this->reader->reveal());
+ $res = $snippet->invoke('projectId');
+
+ $this->assertEquals($res->returnVal(), self::PROJECT);
+ }
+
+ public function testClassMetadata()
+ {
+ $key = 'foo';
+ $val = 'bar';
+
+ $this->reader->read('project/attributes/'. $key)
+ ->shouldBeCalled()
+ ->willReturn($val);
+
+ $this->metadata->setReader($this->reader->reveal());
+
+ $snippet = $this->snippetFromClass(Metadata::class, 1);
+ $snippet->addLocal('metadata', $this->metadata);
+ $snippet->addLocal('key', $key);
+
+ $res = $snippet->invoke('val');
+ $this->assertEquals($res->returnVal(), $val);
+ }
+
+ public function testGet()
+ {
+ $this->reader->read('project/project-id')
+ ->shouldBeCalled()
+ ->willReturn(self::PROJECT);
+
+ $this->metadata->setReader($this->reader->reveal());
+
+ $snippet = $this->snippetFromMethod(Metadata::class, 'get');
+ $snippet->addLocal('metadata', $this->metadata);
+ $res = $snippet->invoke('projectId');
+
+ $this->assertEquals(self::PROJECT, $res->returnVal());
+ }
+
+ public function testGetProjectId()
+ {
+ $this->reader->read('project/project-id')
+ ->shouldBeCalled()
+ ->willReturn(self::PROJECT);
+
+ $this->metadata->setReader($this->reader->reveal());
+
+ $snippet = $this->snippetFromMethod(Metadata::class, 'getProjectId');
+ $snippet->addLocal('metadata', $this->metadata);
+ $res = $snippet->invoke('projectId');
+
+ $this->assertEquals(self::PROJECT, $res->returnVal());
+ }
+
+ public function testGetProjectMetadata()
+ {
+ $val = 'hello world';
+
+ $this->reader->read('project/attributes/foo')
+ ->shouldBeCalled()
+ ->willReturn($val);
+
+ $this->metadata->setReader($this->reader->reveal());
+
+ $snippet = $this->snippetFromMethod(Metadata::class, 'getProjectMetadata');
+ $snippet->addLocal('metadata', $this->metadata);
+ $res = $snippet->invoke('foo');
+
+ $this->assertEquals($val, $res->returnVal());
+ }
+
+ public function testGetInstanceMetadata()
+ {
+ $val = 'hello world';
+
+ $this->reader->read('instance/attributes/foo')
+ ->shouldBeCalled()
+ ->willReturn($val);
+
+ $this->metadata->setReader($this->reader->reveal());
+
+ $snippet = $this->snippetFromMethod(Metadata::class, 'getInstanceMetadata');
+ $snippet->addLocal('metadata', $this->metadata);
+ $res = $snippet->invoke('foo');
+
+ $this->assertEquals($val, $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Iam/IamTest.php b/tests/snippets/Core/Iam/IamTest.php
similarity index 74%
rename from tests/snippets/Iam/IamTest.php
rename to tests/snippets/Core/Iam/IamTest.php
index 0f9602a4390e..a1fcf9722d8a 100644
--- a/tests/snippets/Iam/IamTest.php
+++ b/tests/snippets/Core/Iam/IamTest.php
@@ -15,11 +15,11 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Snippets\Iam;
+namespace Google\Cloud\Tests\Snippets\Core\Iam;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
-use Google\Cloud\Iam\Iam;
-use Google\Cloud\Iam\IamConnectionInterface;
+use Google\Cloud\Core\Iam\Iam;
+use Google\Cloud\Core\Iam\IamConnectionInterface;
use Prophecy\Argument;
/**
@@ -39,8 +39,16 @@ public function setUp()
$this->resource = 'testObject';
$this->connection = $this->prophesize(IamConnectionInterface::class);
- $this->iam = new \IamStub($this->connection->reveal(), $this->resource);
- $this->iam->setConnection($this->connection->reveal());
+ $this->iam = \Google\Cloud\Dev\stub(Iam::class, [$this->connection->reveal(), $this->resource]);
+ $this->iam->___setProperty('connection', $this->connection->reveal());
+ }
+
+ public function testClass()
+ {
+ $snippet = $this->snippetFromClass(Iam::class);
+ $res = $snippet->invoke('iam');
+
+ $this->assertInstanceOf(Iam::class, $res->returnVal());
}
public function testPolicy()
@@ -52,7 +60,7 @@ public function testPolicy()
->shouldBeCalled()
->willReturn('foo');
- $this->iam->setConnection($this->connection->reveal());
+ $this->iam->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('policy');
@@ -62,7 +70,7 @@ public function testPolicy()
$this->assertEquals('foo', $res->returnVal());
}
- public function setPolicy()
+ public function testSetPolicy()
{
$snippet = $this->snippetFromMethod(Iam::class, 'setPolicy');
$snippet->addLocal('iam', $this->iam);
@@ -76,12 +84,15 @@ public function setPolicy()
]);
$this->connection->setPolicy([
- 'bindings' => [
- ['members' => ['user:test@example.com']]
- ]
+ 'policy' => [
+ 'bindings' => [
+ ['members' => 'user:test@example.com']
+ ]
+ ],
+ 'resource' => $this->resource
])->shouldBeCalled()->willReturn('foo');
- $this->iam->setConnection($this->connection->reveal());
+ $this->iam->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('policy');
@@ -105,7 +116,7 @@ public function testTestPermissions()
->shouldBeCalled()
->willReturn($permissions);
- $this->iam->setConnection($this->connection->reveal());
+ $this->iam->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('allowedPermissions');
$this->assertEquals($permissions, $res->returnVal());
@@ -120,7 +131,7 @@ public function testReload()
->shouldBeCalled()
->willReturn('foo');
- $this->iam->setConnection($this->connection->reveal());
+ $this->iam->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('policy');
$this->assertEquals('foo', $res->returnVal());
diff --git a/tests/snippets/Iam/PolicyBuilderTest.php b/tests/snippets/Core/Iam/PolicyBuilderTest.php
similarity index 82%
rename from tests/snippets/Iam/PolicyBuilderTest.php
rename to tests/snippets/Core/Iam/PolicyBuilderTest.php
index db7b94aa0bb6..32bf8bb19d71 100644
--- a/tests/snippets/Iam/PolicyBuilderTest.php
+++ b/tests/snippets/Core/Iam/PolicyBuilderTest.php
@@ -15,10 +15,10 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Snippets\Iam;
+namespace Google\Cloud\Tests\Snippets\Core\Iam;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
-use Google\Cloud\Iam\PolicyBuilder;
+use Google\Cloud\Core\Iam\PolicyBuilder;
/**
* @group iam
@@ -43,6 +43,16 @@ public function testClass()
}
public function testSetBindings()
+ {
+ $snippet = $this->snippetFromMethod(PolicyBuilder::class, 'setBindings');
+ $snippet->addLocal('builder', $this->pb);
+
+ $res = $snippet->invoke();
+ $this->assertEquals('roles/admin', $this->pb->result()['bindings'][0]['role']);
+ $this->assertEquals('user:admin@domain.com', $this->pb->result()['bindings'][0]['members'][0]);
+ }
+
+ public function testAddBindings()
{
$snippet = $this->snippetFromMethod(PolicyBuilder::class, 'addBinding');
$snippet->addLocal('builder', $this->pb);
diff --git a/tests/snippets/Core/Int64Test.php b/tests/snippets/Core/Int64Test.php
new file mode 100644
index 000000000000..d9d10a22f9a6
--- /dev/null
+++ b/tests/snippets/Core/Int64Test.php
@@ -0,0 +1,52 @@
+int64 = new Int64((string)self::VALUE);
+ }
+
+ public function testClass()
+ {
+ $snippet = $this->snippetFromClass(Int64::class);
+ $snippet->addUse(Int64::class);
+ $this->assertInstanceOf(Int64::class, $snippet->invoke('int64')->returnVal());
+ }
+
+ public function testGet()
+ {
+ $snippet = $this->snippetFromMethod(Int64::class, 'get');
+ $snippet->addLocal('int64', $this->int64);
+ $res = $snippet->invoke('value');
+
+ $this->assertEquals((string)self::VALUE, $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Datastore/DatastoreClientTest.php b/tests/snippets/Datastore/DatastoreClientTest.php
index fb5482d01499..e12caf10c27c 100644
--- a/tests/snippets/Datastore/DatastoreClientTest.php
+++ b/tests/snippets/Datastore/DatastoreClientTest.php
@@ -30,6 +30,7 @@
use Google\Cloud\Datastore\Query\QueryInterface;
use Google\Cloud\Datastore\Transaction;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
+use Google\Cloud\Core\Int64;
use Prophecy\Argument;
/**
@@ -67,17 +68,9 @@ public function testClass()
$this->assertInstanceOf(DatastoreClient::class, $res->returnVal());
}
- public function testClassDirectInstantiation()
- {
- $snippet = $this->snippetFromClass(DatastoreClient::class, 1);
- $res = $snippet->invoke('datastore');
-
- $this->assertInstanceOf(DatastoreClient::class, $res->returnVal());
- }
-
public function testMultiTenant()
{
- $snippet = $this->snippetFromClass(DatastoreClient::class, 2);
+ $snippet = $this->snippetFromClass(DatastoreClient::class, 1);
$res = $snippet->invoke('datastore');
$this->assertInstanceOf(DatastoreClient::class, $res->returnVal());
@@ -99,7 +92,7 @@ public function testMultiTenant()
public function testEmulator()
{
- $snippet = $this->snippetFromClass(DatastoreClient::class, 3);
+ $snippet = $this->snippetFromClass(DatastoreClient::class, 2);
$res = $snippet->invoke('datastore');
$this->assertInstanceOf(DatastoreClient::class, $res->returnVal());
@@ -234,6 +227,24 @@ public function testBlob()
{
$snippet = $this->snippetFromMethod(DatastoreClient::class, 'blob');
$snippet->addLocal('datastore', $this->client);
+
+ $res = $snippet->invoke('blob');
+ $this->assertInstanceOf(Blob::class, $res->returnVal());
+ }
+
+ public function testInt64()
+ {
+ $snippet = $this->snippetFromMethod(DatastoreClient::class, 'int64');
+ $snippet->addLocal('datastore', $this->client);
+
+ $res = $snippet->invoke('int64');
+ $this->assertInstanceOf(Int64::class, $res->returnVal());
+ }
+
+ public function testBlobWithFile()
+ {
+ $snippet = $this->snippetFromMethod(DatastoreClient::class, 'blob', 1);
+ $snippet->addLocal('datastore', $this->client);
$snippet->replace("file_get_contents(__DIR__ .'/family-photo.jpg')", "''");
$res = $snippet->invoke('blob');
diff --git a/tests/snippets/Datastore/Query/GqlQueryTest.php b/tests/snippets/Datastore/Query/GqlQueryTest.php
index d7f9fd77a0bf..d8158e42bff9 100644
--- a/tests/snippets/Datastore/Query/GqlQueryTest.php
+++ b/tests/snippets/Datastore/Query/GqlQueryTest.php
@@ -19,7 +19,9 @@
use Google\Cloud\Datastore\Connection\ConnectionInterface;
use Google\Cloud\Datastore\DatastoreClient;
+use Google\Cloud\Datastore\EntityIterator;
use Google\Cloud\Datastore\EntityMapper;
+use Google\Cloud\Datastore\Operation;
use Google\Cloud\Datastore\Query\GqlQuery;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
use Prophecy\Argument;
@@ -37,12 +39,12 @@ public function setUp()
{
$this->datastore = new DatastoreClient;
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->operation = new \OperationStub(
+ $this->operation = \Google\Cloud\Dev\Stub(Operation::class, [
$this->connection->reveal(),
'my-awesome-project',
'',
new EntityMapper('my-awesome-project', true, false)
- );
+ ]);
}
public function testClass()
@@ -68,7 +70,7 @@ public function testClass()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$snippet = $this->snippetFromClass(GqlQuery::class);
$snippet->addLocal('operation', $this->operation);
@@ -81,7 +83,7 @@ public function testClass()
$res = $snippet->invoke(['query', 'res']);
$this->assertEquals('Google', $res->output());
- $this->assertInstanceOf(\Generator::class, $res->returnVal()[1]);
+ $this->assertInstanceOf(EntityIterator::class, $res->returnVal()[1]);
$this->assertTrue(array_key_exists('namedBindings', $res->returnVal()[0]->queryObject()));
}
diff --git a/tests/snippets/Datastore/Query/QueryTest.php b/tests/snippets/Datastore/Query/QueryTest.php
index 4c64e9e37078..d7074c6e5079 100644
--- a/tests/snippets/Datastore/Query/QueryTest.php
+++ b/tests/snippets/Datastore/Query/QueryTest.php
@@ -19,8 +19,10 @@
use Google\Cloud\Datastore\Connection\ConnectionInterface;
use Google\Cloud\Datastore\DatastoreClient;
+use Google\Cloud\Datastore\EntityIterator;
use Google\Cloud\Datastore\EntityMapper;
use Google\Cloud\Datastore\Key;
+use Google\Cloud\Datastore\Operation;
use Google\Cloud\Datastore\Query\Query;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
use Prophecy\Argument;
@@ -41,12 +43,12 @@ public function setUp()
$this->datastore = new DatastoreClient;
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->operation = new \OperationStub(
+ $this->operation = \Google\Cloud\Dev\stub(Operation::class, [
$this->connection->reveal(),
'my-awesome-project',
'',
$mapper
- );
+ ]);
$this->query = new Query($mapper);
}
@@ -75,7 +77,7 @@ public function testClass()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$snippet = $this->snippetFromClass(Query::class);
$snippet->addLocal('operation', $this->operation);
@@ -88,7 +90,7 @@ public function testClass()
$res = $snippet->invoke('res');
$this->assertEquals('Google', $res->output());
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(EntityIterator::class, $res->returnVal());
}
public function testClassQueryObject()
diff --git a/tests/snippets/Datastore/TransactionTest.php b/tests/snippets/Datastore/TransactionTest.php
index f15784a8fd6c..a6f701677d79 100644
--- a/tests/snippets/Datastore/TransactionTest.php
+++ b/tests/snippets/Datastore/TransactionTest.php
@@ -44,12 +44,12 @@ class TransactionTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->operation = new \OperationStub(
+ $this->operation = \Google\Cloud\Dev\stub(Operation::class, [
$this->connection->reveal(),
self::PROJECT,
'',
new EntityMapper(self::PROJECT, false, false)
- );
+ ]);
$this->transaction = new Transaction($this->operation, self::PROJECT, $this->transactionId);
$this->datastore = new DatastoreClient;
$this->key = new Key('my-awesome-project', [
@@ -101,7 +101,7 @@ public function testInsert()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -126,7 +126,7 @@ public function testInsertBatch()
$this->allocateIdsConnectionMock();
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -152,7 +152,7 @@ public function testUpdate()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -198,7 +198,7 @@ public function testUpsert()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -241,7 +241,7 @@ public function testDelete()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -292,7 +292,7 @@ public function testLookup()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Bob', $res->output());
@@ -342,7 +342,7 @@ public function testLookupBatch()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals("Bob", explode("\n", $res->output())[0]);
@@ -382,7 +382,7 @@ public function testRunQuery()
]
]);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('result');
$this->assertEquals('Bob', $res->output());
@@ -396,7 +396,7 @@ public function testCommit()
$this->connection->commit(Argument::any())
->shouldBeCalled();
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -409,7 +409,7 @@ public function testRollback()
$this->connection->rollback(Argument::any())
->shouldBeCalled();
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
diff --git a/tests/snippets/Logging/LoggerTest.php b/tests/snippets/Logging/LoggerTest.php
index 972fb4367457..5c74fcd2d3a9 100644
--- a/tests/snippets/Logging/LoggerTest.php
+++ b/tests/snippets/Logging/LoggerTest.php
@@ -21,6 +21,7 @@
use Google\Cloud\Logging\Connection\ConnectionInterface;
use Google\Cloud\Logging\Entry;
use Google\Cloud\Logging\Logger;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Prophecy\Argument;
/**
@@ -37,11 +38,11 @@ class LoggerTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->logger = new \LoggerStub(
+ $this->logger = \Google\Cloud\Dev\stub(Logger::class, [
$this->connection->reveal(),
self::NAME,
self::PROJECT
- );
+ ]);
}
public function testClass()
@@ -60,7 +61,7 @@ public function testDelete()
$this->connection->deleteLog(Argument::any())
->shouldBeCalled();
- $this->logger->setConnection($this->connection->reveal());
+ $this->logger->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -79,10 +80,10 @@ public function testEntries()
]
]);
- $this->logger->setConnection($this->connection->reveal());
+ $this->logger->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('entries');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('foo', explode(PHP_EOL, $res->output())[0]);
$this->assertEquals('bar', explode(PHP_EOL, $res->output())[1]);
}
@@ -105,10 +106,10 @@ public function testEntriesWithFilter()
]
]);
- $this->logger->setConnection($this->connection->reveal());
+ $this->logger->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('entries');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('foo', explode(PHP_EOL, $res->output())[0]);
$this->assertEquals('bar', explode(PHP_EOL, $res->output())[1]);
}
@@ -149,7 +150,7 @@ public function testWrite()
$this->connection->writeEntries(Argument::any())
->shouldBeCalled();
- $this->logger->setConnection($this->connection->reveal());
+ $this->logger->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -163,7 +164,7 @@ public function testWriteKeyValLevel()
$this->connection->writeEntries(Argument::any())
->shouldBeCalled();
- $this->logger->setConnection($this->connection->reveal());
+ $this->logger->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -176,7 +177,7 @@ public function testWriteFactory()
$this->connection->writeEntries(Argument::any())
->shouldBeCalled();
- $this->logger->setConnection($this->connection->reveal());
+ $this->logger->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -189,7 +190,7 @@ public function testWriteBatch()
$this->connection->writeEntries(Argument::any())
->shouldBeCalled();
- $this->logger->setConnection($this->connection->reveal());
+ $this->logger->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
diff --git a/tests/snippets/Logging/LoggingClientTest.php b/tests/snippets/Logging/LoggingClientTest.php
index 585ec9d4b0d5..48cc81a34a7b 100644
--- a/tests/snippets/Logging/LoggingClientTest.php
+++ b/tests/snippets/Logging/LoggingClientTest.php
@@ -24,6 +24,7 @@
use Google\Cloud\Logging\Metric;
use Google\Cloud\Logging\PsrLogger;
use Google\Cloud\Logging\Sink;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Prophecy\Argument;
/**
@@ -37,8 +38,8 @@ class LoggingClientTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->client = new \LoggingClientStub;
- $this->client->setConnection($this->connection->reveal());
+ $this->client = \Google\Cloud\Dev\stub(LoggingClient::class);
+ $this->client->___setProperty('connection', $this->connection->reveal());
}
public function testClass()
@@ -49,14 +50,6 @@ public function testClass()
$this->assertInstanceOf(LoggingClient::class, $res->returnVal());
}
- public function testClassDirectInstantiation()
- {
- $snippet = $this->snippetFromClass(LoggingClient::class, 1);
- $res = $snippet->invoke('logging');
-
- $this->assertInstanceOf(LoggingClient::class, $res->returnVal());
- }
-
public function testCreateSink()
{
$snippet = $this->snippetFromMethod(LoggingClient::class, 'createSink');
@@ -93,10 +86,10 @@ public function testSinks()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('sinks');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('Sink 1', explode(PHP_EOL, $res->output())[0]);
$this->assertEquals('Sink 2', explode(PHP_EOL, $res->output())[1]);
}
@@ -110,7 +103,7 @@ public function testCreateMetric()
->shouldBeCalled()
->willReturn([]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('metric');
$this->assertInstanceOf(Metric::class, $res->returnVal());
@@ -139,10 +132,10 @@ public function testMetrics()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('metrics');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('Metric 1', explode(PHP_EOL, $res->output())[0]);
$this->assertEquals('Metric 2', explode(PHP_EOL, $res->output())[1]);
}
@@ -161,10 +154,10 @@ public function testEntries()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('entries');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('Entry 1', explode(PHP_EOL, $res->output())[0]);
$this->assertEquals('Entry 2', explode(PHP_EOL, $res->output())[1]);
}
@@ -186,10 +179,10 @@ public function testEntriesWithFilter()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('entries');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('Entry 1', explode(PHP_EOL, $res->output())[0]);
$this->assertEquals('Entry 2', explode(PHP_EOL, $res->output())[1]);
}
diff --git a/tests/snippets/Logging/MetricTest.php b/tests/snippets/Logging/MetricTest.php
index 747dadaffb38..240cf7c890d2 100644
--- a/tests/snippets/Logging/MetricTest.php
+++ b/tests/snippets/Logging/MetricTest.php
@@ -36,11 +36,11 @@ class MetricTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->metric = new \MetricStub(
+ $this->metric = \Google\Cloud\Dev\stub(Metric::class, [
$this->connection->reveal(),
self::METRIC,
self::PROJECT
- );
+ ]);
}
public function testClass()
@@ -60,7 +60,7 @@ public function testExists()
->shouldBeCalled()
->willReturn([]);
- $this->metric->setConnection($this->connection->reveal());
+ $this->metric->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals("Metric exists!", $res->output());
@@ -74,7 +74,7 @@ public function testDelete()
$this->connection->deleteMetric(Argument::any())
->shouldBeCalled();
- $this->metric->setConnection($this->connection->reveal());
+ $this->metric->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -92,7 +92,7 @@ public function testUpdate()
->shouldBeCalled()
->willReturn(['description' => 'Foo']);
- $this->metric->setConnection($this->connection->reveal());
+ $this->metric->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -106,7 +106,7 @@ public function testInfo()
->shouldBeCalled()
->willReturn(['description' => 'Foo']);
- $this->metric->setConnection($this->connection->reveal());
+ $this->metric->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Foo', $res->output());
@@ -121,7 +121,7 @@ public function testReload()
->shouldBeCalled()
->willReturn(['description' => 'Foo']);
- $this->metric->setConnection($this->connection->reveal());
+ $this->metric->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Foo', $res->output());
diff --git a/tests/snippets/Logging/PsrLoggerTest.php b/tests/snippets/Logging/PsrLoggerTest.php
index 3fb0a451976d..a33a505779cb 100644
--- a/tests/snippets/Logging/PsrLoggerTest.php
+++ b/tests/snippets/Logging/PsrLoggerTest.php
@@ -64,6 +64,21 @@ public function testEmergency()
$snippet->invoke();
}
+ public function testAlert()
+ {
+ $snippet = $this->snippetFromMethod(PsrLogger::class, 'alert');
+ $snippet->addLocal('psrLogger', $this->psr);
+
+ $this->connection->writeEntries(Argument::that(function ($args) {
+ if ($args['entries'][0]['severity'] !== Logger::ALERT) return false;
+ return true;
+ }))->shouldBeCalled();
+
+ $this->psr->setConnection($this->connection->reveal());
+
+ $snippet->invoke();
+ }
+
public function testCritical()
{
$snippet = $this->snippetFromMethod(PsrLogger::class, 'critical');
diff --git a/tests/snippets/Logging/SinkTest.php b/tests/snippets/Logging/SinkTest.php
index 1a8f9c2cc345..91cd4c77392a 100644
--- a/tests/snippets/Logging/SinkTest.php
+++ b/tests/snippets/Logging/SinkTest.php
@@ -36,7 +36,11 @@ class SinkTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->sink = new \SinkStub($this->connection->reveal(), self::SINK, self::PROJECT);
+ $this->sink = \Google\Cloud\Dev\stub(Sink::class, [
+ $this->connection->reveal(),
+ self::SINK,
+ self::PROJECT
+ ]);
}
public function testClass()
@@ -56,7 +60,7 @@ public function testExists()
->shouldBeCalled()
->willReturn([]);
- $this->sink->setConnection($this->connection->reveal());
+ $this->sink->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Sink exists!', $res->output());
@@ -70,7 +74,7 @@ public function testDelete()
$this->connection->deleteSink(Argument::any())
->shouldBeCalled();
- $this->sink->setConnection($this->connection->reveal());
+ $this->sink->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -89,7 +93,7 @@ public function testUpdate()
'destination' => 'Foo'
]);
- $this->sink->setConnection($this->connection->reveal());
+ $this->sink->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -105,7 +109,7 @@ public function testInfo()
'destination' => 'Foo'
]);
- $this->sink->setConnection($this->connection->reveal());
+ $this->sink->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Foo', $res->output());
@@ -122,7 +126,7 @@ public function testReload()
'destination' => 'Foo'
]);
- $this->sink->setConnection($this->connection->reveal());
+ $this->sink->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Foo', $res->output());
diff --git a/tests/snippets/NaturalLanguage/AnnotationTest.php b/tests/snippets/NaturalLanguage/AnnotationTest.php
new file mode 100644
index 000000000000..cc37edc7e823
--- /dev/null
+++ b/tests/snippets/NaturalLanguage/AnnotationTest.php
@@ -0,0 +1,163 @@
+info = [
+ 'sentences' => [
+ [
+ 'text' => [
+ 'content' => 'hello world'
+ ]
+ ]
+ ],
+ 'tokens' => [
+ [
+ 'text' => [
+ 'content' => 'hello world'
+ ],
+ 'partOfSpeech' => [
+ 'tag' => 'NOUN'
+ ],
+ 'dependencyEdge' => [
+ 'label' => 'P'
+ ],
+ 'lemma' => 'foo'
+ ]
+ ],
+ 'entities' => [
+ [
+ 'type' => 'PERSON',
+ 'name' => 'somebody'
+ ]
+ ],
+ 'language' => 'en-us',
+ 'documentSentiment' => [
+ 'score' => 999
+ ]
+ ];
+ $this->annotation = new Annotation($this->info);
+ }
+
+ public function testClass()
+ {
+ $connection = $this->prophesize(ConnectionInterface::class);
+ $connection->annotateText(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([]);
+
+ $snippet = $this->snippetFromClass(Annotation::class);
+ $snippet->addLocal('connectionStub', $connection->reveal());
+ $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($language);
+ $property = $reflection->getProperty(\'connection\');
+ $property->setAccessible(true);
+ $property->setValue($language, $connectionStub);
+ $property->setAccessible(false);'
+ );
+
+ $res = $snippet->invoke('annotation');
+ $this->assertInstanceOf(Annotation::class, $res->returnVal());
+ }
+
+ public function testSentences()
+ {
+ $snippet = $this->snippetFromMagicMethod(Annotation::class, 'sentences');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke();
+ $this->assertEquals($this->info['sentences'][0]['text']['content'], $res->output());
+ }
+
+ public function testTokens()
+ {
+ $snippet = $this->snippetFromMagicMethod(Annotation::class, 'tokens');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke();
+ $this->assertEquals($this->info['tokens'][0]['text']['content'], $res->output());
+ }
+
+ public function testEntities()
+ {
+ $snippet = $this->snippetFromMagicMethod(Annotation::class, 'entities');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke();
+ $this->assertEquals($this->info['entities'][0]['type'], $res->output());
+ }
+
+ public function testLanguage()
+ {
+ $snippet = $this->snippetFromMagicMethod(Annotation::class, 'language');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke();
+ $this->assertEquals($this->info['language'], $res->output());
+ }
+
+ public function testInfo()
+ {
+ $snippet = $this->snippetFromMethod(Annotation::class, 'info');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke('info');
+
+ $this->assertEquals($this->info, $res->returnVal());
+ }
+
+ public function testSentiment()
+ {
+ $snippet = $this->snippetFromMethod(Annotation::class, 'sentiment');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke();
+ $this->assertEquals('This is a positive message.', $res->output());
+ }
+
+ public function testTokensByTag()
+ {
+ $snippet = $this->snippetFromMethod(Annotation::class, 'tokensByTag');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke();
+ $this->assertEquals($this->info['tokens'][0]['lemma'], $res->output());
+ }
+
+ public function testTokensByLabel()
+ {
+ $snippet = $this->snippetFromMethod(Annotation::class, 'tokensByLabel');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke();
+ $this->assertEquals($this->info['tokens'][0]['lemma'], $res->output());
+ }
+
+ public function testEntitiesByType()
+ {
+ $snippet = $this->snippetFromMethod(Annotation::class, 'entitiesByType');
+ $snippet->addLocal('annotation', $this->annotation);
+ $res = $snippet->invoke();
+ $this->assertEquals($this->info['entities'][0]['name'], $res->output());
+ }
+}
diff --git a/tests/snippets/NaturalLanguage/NaturalLanguageClientTest.php b/tests/snippets/NaturalLanguage/NaturalLanguageClientTest.php
new file mode 100644
index 000000000000..f2d216e82c03
--- /dev/null
+++ b/tests/snippets/NaturalLanguage/NaturalLanguageClientTest.php
@@ -0,0 +1,149 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->client = \Google\Cloud\Dev\stub(NaturalLanguageClient::class);
+ $this->client->___setProperty('connection', $this->connection->reveal());
+ }
+
+ public function testClass()
+ {
+ $snippet = $this->snippetFromClass(NaturalLanguageClient::class);
+ $res = $snippet->invoke('language');
+ $this->assertInstanceOf(NaturalLanguageClient::class, $res->returnVal());
+ }
+
+ public function testAnalyzeEntities()
+ {
+ $this->connection->analyzeEntities(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'entities' => [
+ [
+ 'type' => 'PERSON'
+ ]
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'analyzeEntities');
+ $snippet->addLocal('language', $this->client);
+
+ $res = $snippet->invoke();
+ $this->assertEquals('PERSON', $res->output());
+ }
+
+ public function testAnalyzeSentiment()
+ {
+ $this->connection->analyzeSentiment(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'documentSentiment' => [
+ 'score' => 1.0
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'analyzeSentiment');
+ $snippet->addLocal('language', $this->client);
+
+ $res = $snippet->invoke();
+ $this->assertEquals("This is a positive message.", $res->output());
+ }
+
+ public function testAnalyzeSyntax()
+ {
+ $this->connection->analyzeSyntax(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'sentences' => [
+ [
+ 'text' => [
+ 'beginOffset' => 1.0
+ ]
+ ]
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'analyzeSyntax');
+ $snippet->addLocal('language', $this->client);
+
+ $res = $snippet->invoke();
+ $this->assertEquals('1.0', $res->output());
+ }
+
+ public function testAnnotateTextAllFeatures()
+ {
+ $this->connection->annotateText(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'documentSentiment' => [
+ 'magnitude' => 999
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'annotateText');
+ $snippet->addLocal('language', $this->client);
+
+ $this->assertEquals('999', $snippet->invoke()->output());
+ }
+
+ public function testAnnotateTextSomeFeatures()
+ {
+ $this->connection->annotateText(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'tokens' => [
+ [
+ 'text' => [
+ 'beginOffset' => '2.0'
+ ]
+ ]
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'annotateText', 1);
+ $snippet->addLocal('language', $this->client);
+
+ $this->assertEquals('2.0', $snippet->invoke()->output());
+ }
+}
diff --git a/tests/snippets/PubSub/MessageTest.php b/tests/snippets/PubSub/MessageTest.php
index 3a247c8a4c64..4b8fdeda2870 100644
--- a/tests/snippets/PubSub/MessageTest.php
+++ b/tests/snippets/PubSub/MessageTest.php
@@ -73,7 +73,7 @@ public function testClass()
$snippet = $this->snippetFromClass(Message::class);
$snippet->addLocal('connectionStub', $connection->reveal());
- $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($pubsub);
+ $snippet->insertAfterLine(2, '$reflection = new \ReflectionClass($pubsub);
$property = $reflection->getProperty(\'connection\');
$property->setAccessible(true);
$property->setValue($pubsub, $connectionStub);
@@ -85,7 +85,7 @@ public function testClass()
);
$res = $snippet->invoke('messages');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertContainsOnlyInstancesOf(Message::class, $res->returnVal());
$this->assertEquals('hello world', $res->output());
}
diff --git a/tests/snippets/PubSub/PubSubClientTest.php b/tests/snippets/PubSub/PubSubClientTest.php
index 5f8969ef8c6d..08788c5f0c0d 100644
--- a/tests/snippets/PubSub/PubSubClientTest.php
+++ b/tests/snippets/PubSub/PubSubClientTest.php
@@ -24,6 +24,7 @@
use Google\Cloud\PubSub\PubSubClient;
use Google\Cloud\PubSub\Subscription;
use Google\Cloud\PubSub\Topic;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Prophecy\Argument;
/**
@@ -40,12 +41,21 @@ class PubSubClientTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->client = new \PubSubClientStub(['transport' => 'rest']);
+ $this->client = \Google\Cloud\Dev\stub(PubSubClient::class, [['transport' => 'rest']]);
}
- public function testClassExample1()
+ public function testClassExample()
{
- $snippet = $this->snippetFromClass(PubSubClient::class, '__construct');
+ $snippet = $this->snippetFromClass(PubSubClient::class);
+ $res = $snippet->invoke('pubsub');
+
+ $this->assertInstanceOf(PubSubClient::class, $res->returnVal());
+ }
+
+ // phpunit doesn't get the value of $_ENV, so testing PUBSUB_EMULATOR_HOST is pretty tough.
+ public function testClassExample3()
+ {
+ $snippet = $this->snippetFromClass(PubSubClient::class, 1);
$res = $snippet->invoke('pubsub');
$this->assertInstanceOf(PubSubClient::class, $res->returnVal());
@@ -59,7 +69,7 @@ public function testCreateTopic()
'name' => self::TOPIC
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$snippet = $this->snippetFromMethod(PubSubClient::class, 'createTopic');
$snippet->addLocal('pubsub', $this->client);
@@ -82,7 +92,7 @@ public function testTopic()
'name' => self::TOPIC
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('topic');
@@ -104,11 +114,11 @@ public function testTopics()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('topics');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals(self::TOPIC, $res->output());
}
@@ -124,7 +134,7 @@ public function testSubscribe()
'topic' => self::TOPIC
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('subscription');
@@ -146,7 +156,7 @@ public function testSubscription()
'topic' => self::TOPIC
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('subscription');
@@ -169,10 +179,10 @@ public function testSubscriptions()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('subscriptions');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals(self::SUBSCRIPTION, $res->output());
}
diff --git a/tests/snippets/PubSub/SubscriptionTest.php b/tests/snippets/PubSub/SubscriptionTest.php
index 04684afd8eb8..a6229f76c895 100644
--- a/tests/snippets/PubSub/SubscriptionTest.php
+++ b/tests/snippets/PubSub/SubscriptionTest.php
@@ -19,8 +19,9 @@
use Google\Cloud\Dev\SetStubConnectionTrait;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
-use Google\Cloud\Iam\Iam;
+use Google\Cloud\Core\Iam\Iam;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
+use Google\Cloud\PubSub\Message;
use Google\Cloud\PubSub\PubSubClient;
use Google\Cloud\PubSub\Subscription;
use Prophecy\Argument;
@@ -40,16 +41,16 @@ class SubscriptionTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->subscription = new \SubscriptionStub(
+ $this->subscription = \Google\Cloud\Dev\stub(Subscription::class, [
$this->connection->reveal(),
'foo',
self::SUBSCRIPTION,
self::TOPIC,
false
- );
+ ]);
- $this->pubsub = new \PubSubClientStub(['transport' => 'rest']);
- $this->pubsub->setConnection($this->connection->reveal());
+ $this->pubsub = \Google\Cloud\Dev\stub(PubSubClient::class, [['transport' => 'rest']]);
+ $this->pubsub->___setProperty('connection', $this->connection->reveal());
}
public function testClassThroughTopic()
@@ -88,7 +89,7 @@ public function testCreate()
->shouldBeCalled()
->willReturn($return);
- $this->pubsub->setConnection($this->connection->reveal());
+ $this->pubsub->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('result');
$this->assertEquals($return, $res->returnVal());
@@ -102,7 +103,7 @@ public function testDelete()
$this->connection->deleteSubscription(Argument::any())
->shouldBeCalled();
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -115,7 +116,7 @@ public function testExists()
$this->connection->getSubscription(Argument::any())
->shouldBeCalled();
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Subscription exists!', $res->output());
@@ -130,7 +131,7 @@ public function testInfo()
->shouldBeCalled()
->willReturn(['name' => self::SUBSCRIPTION]);
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals(self::SUBSCRIPTION, $res->output());
@@ -145,7 +146,7 @@ public function testReload()
->shouldBeCalled()
->willReturn(['name' => self::SUBSCRIPTION]);
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
@@ -166,10 +167,10 @@ public function testPull()
]
]);
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('messages');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertContainsOnlyInstancesOf(Message::class, $res->returnVal());
$this->assertEquals('hello world', $res->output());
}
@@ -189,7 +190,7 @@ public function testAcknowledge()
$this->connection->acknowledge(Argument::any())
->shouldBeCalled();
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -210,7 +211,7 @@ public function testAcknowledgeBatch()
$this->connection->acknowledge(Argument::any())
->shouldBeCalled();
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -234,14 +235,14 @@ public function testModifyAckDeadline()
$this->connection->modifyAckDeadline(Argument::any())
->shouldBeCalled();
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
public function testModifyAckDeadlineBatch()
{
- $snippet = $this->snippetFromMethod(Subscription::class, 'modifyAckDeadline');
+ $snippet = $this->snippetFromMethod(Subscription::class, 'modifyAckDeadlineBatch');
$snippet->addLocal('subscription', $this->subscription);
$this->connection->pull(Argument::any())
@@ -258,7 +259,7 @@ public function testModifyAckDeadlineBatch()
$this->connection->modifyAckDeadline(Argument::any())
->shouldBeCalled();
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -271,7 +272,7 @@ public function testModifyPushConfig()
$this->connection->modifyPushConfig(Argument::any())
->shouldBeCalled();
- $this->subscription->setConnection($this->connection->reveal());
+ $this->subscription->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
diff --git a/tests/snippets/PubSub/TopicTest.php b/tests/snippets/PubSub/TopicTest.php
index aed6f638b7aa..64e3d3595d7b 100644
--- a/tests/snippets/PubSub/TopicTest.php
+++ b/tests/snippets/PubSub/TopicTest.php
@@ -17,11 +17,13 @@
namespace Google\Cloud\Tests\Snippets\PubSub;
+use Google\Cloud\Core\Iam\Iam;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
-use Google\Cloud\Iam\Iam;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
+use Google\Cloud\PubSub\PubSubClient;
use Google\Cloud\PubSub\Subscription;
use Google\Cloud\PubSub\Topic;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Prophecy\Argument;
/**
@@ -33,23 +35,25 @@ class TopicTest extends SnippetTestCase
const SUBSCRIPTION = 'projects/my-awesome-project/subscriptions/my-new-subscription';
private $connection;
+ private $pubsub;
private $topic;
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->topic = new \TopicStub(
+ $this->pubsub = \Google\Cloud\Dev\stub(PubSubClient::class);
+ $this->topic = \Google\Cloud\Dev\stub(Topic::class, [
$this->connection->reveal(),
'my-awesome-project',
self::TOPIC,
false
- );
+ ]);
}
public function testClass()
{
$snippet = $this->snippetFromClass(Topic::class);
- $snippet->addLocal('pubsub', new \PubSubClientStub(['transport' => 'rest']));
+ $snippet->addLocal('pubsub', $this->pubsub);
$res = $snippet->invoke('topic');
$this->assertInstanceOf(Topic::class, $res->returnVal());
@@ -59,7 +63,7 @@ public function testClass()
public function testClassWithFullyQualifiedName()
{
$snippet = $this->snippetFromClass(Topic::class, 1);
- $snippet->addLocal('pubsub', new \PubSubClientStub(['transport' => 'rest']));
+ $snippet->addLocal('pubsub', $this->pubsub);
$res = $snippet->invoke('topic');
$this->assertInstanceOf(Topic::class, $res->returnVal());
@@ -84,7 +88,7 @@ public function testCreate()
->shouldBeCalled()
->willReturn([]);
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('topicInfo');
$this->assertEquals([], $res->returnVal());
@@ -98,7 +102,7 @@ public function testDelete()
$this->connection->deleteTopic(Argument::any())
->shouldBeCalled();
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -112,7 +116,7 @@ public function testExists()
->shouldBeCalled()
->willReturn([]);
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Topic exists', $res->output());
@@ -129,7 +133,7 @@ public function testInfo()
'name' => self::TOPIC
]);
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals(self::TOPIC, $res->output());
@@ -146,7 +150,7 @@ public function testReload()
'name' => self::TOPIC
]);
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals(self::TOPIC, $res->output());
@@ -160,7 +164,7 @@ public function testPublish()
$this->connection->publishMessage(Argument::any())
->shouldBeCalled();
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -173,7 +177,7 @@ public function testPublishBatch()
$this->connection->publishMessage(Argument::any())
->shouldBeCalled();
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -189,7 +193,7 @@ public function testSubscribe()
'name' => self::SUBSCRIPTION
]);
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('subscription');
$this->assertInstanceOf(Subscription::class, $res->returnVal());
@@ -217,14 +221,14 @@ public function testSubscriptions()
]
]);
- $this->topic->setConnection($this->connection->reveal());
+ $this->topic->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('subscriptions');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals(self::SUBSCRIPTION, $res->output());
}
- public function iam()
+ public function testIam()
{
$snippet = $this->snippetFromMethod(Topic::class, 'iam');
$snippet->addLocal('topic', $this->topic);
diff --git a/tests/snippets/ServiceBuilderTest.php b/tests/snippets/ServiceBuilderTest.php
new file mode 100644
index 000000000000..7443510ada73
--- /dev/null
+++ b/tests/snippets/ServiceBuilderTest.php
@@ -0,0 +1,76 @@
+cloud = new ServiceBuilder;
+ }
+
+ public function testConstructor()
+ {
+ $snippet = $this->snippetFromMethod(ServiceBuilder::class, '__construct');
+ $this->assertInstanceOf(ServiceBuilder::class, $snippet->invoke('cloud')->returnVal());
+ }
+
+ public function serviceBuilderMethods()
+ {
+ return [
+ ['bigQuery', BigQueryClient::class, 'bigQuery'],
+ ['datastore', DatastoreClient::class, 'datastore'],
+ ['logging', LoggingClient::class, 'logging'],
+ ['naturalLanguage', NaturalLanguageClient::class, 'language'],
+ ['pubsub', PubSubClient::class, 'pubsub'],
+ ['speech', SpeechClient::class, 'speech'],
+ ['storage', StorageClient::class, 'storage'],
+ ['vision', VisionClient::class, 'vision'],
+ ['translate', TranslateClient::class, 'translate']
+ ];
+ }
+
+ /**
+ * @dataProvider serviceBuilderMethods
+ */
+ public function testServices($method, $returnType, $returnName)
+ {
+ $snippet = $this->snippetFromMethod(ServiceBuilder::class, $method);
+ $snippet->addLocal('cloud', $this->cloud);
+ $res = $snippet->invoke($returnName);
+
+ $this->assertInstanceOf($returnType, $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Spanner/ConfigurationTest.php b/tests/snippets/Spanner/ConfigurationTest.php
new file mode 100644
index 000000000000..181b5fecebdf
--- /dev/null
+++ b/tests/snippets/Spanner/ConfigurationTest.php
@@ -0,0 +1,122 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->config = \Google\Cloud\Dev\stub(Configuration::class, [
+ $this->connection->reveal(),
+ self::PROJECT,
+ self::CONFIG
+ ]);
+ }
+
+ public function testClass()
+ {
+ $snippet = $this->snippetFromClass(Configuration::class);
+ $res = $snippet->invoke('configuration');
+
+ $this->assertInstanceOf(Configuration::class, $res->returnVal());
+ $this->assertEquals(self::CONFIG, $res->returnVal()->name());
+ }
+
+ public function testName()
+ {
+ $snippet = $this->snippetFromMethod(Configuration::class, 'name');
+ $snippet->addLocal('configuration', $this->config);
+
+ $res = $snippet->invoke('name');
+ $this->assertEquals(self::CONFIG, $res->returnVal());
+ }
+
+ public function testInfo()
+ {
+ $snippet = $this->snippetFromMethod(Configuration::class, 'info');
+ $snippet->addLocal('configuration', $this->config);
+
+ $info = [
+ 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG,
+ 'displayName' => self::CONFIG
+ ];
+
+ $this->connection->getConfig(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn($info);
+
+ $this->config->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('info');
+ $this->assertEquals($info, $res->returnVal());
+ }
+
+ public function testExists()
+ {
+ $snippet = $this->snippetFromMethod(Configuration::class, 'exists');
+ $snippet->addLocal('configuration', $this->config);
+
+ $this->connection->getConfig(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG,
+ 'displayName' => self::CONFIG
+ ]);
+
+ $this->config->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke();
+ $this->assertEquals('Configuration exists!', $res->output());
+ }
+
+ public function testReload()
+ {
+ $info = [
+ 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG,
+ 'displayName' => self::CONFIG
+ ];
+
+ $snippet = $this->snippetFromMethod(Configuration::class, 'reload');
+ $snippet->addLocal('configuration', $this->config);
+
+ $this->connection->getConfig(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn($info);
+
+ $this->config->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('info');
+ $this->assertEquals($info, $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Spanner/DatabaseTest.php b/tests/snippets/Spanner/DatabaseTest.php
new file mode 100644
index 000000000000..855a0916be75
--- /dev/null
+++ b/tests/snippets/Spanner/DatabaseTest.php
@@ -0,0 +1,151 @@
+prophesize(Instance::class);
+ $instance->name()->willReturn(self::INSTANCE);
+
+ $this->connection = $this->prophesize(ConnectionInterface::class);
+ $this->database = \Google\Cloud\Dev\stub(Database::class, [
+ $this->connection->reveal(),
+ $instance->reveal(),
+ $this->prophesize(SessionPoolInterface::class)->reveal(),
+ $this->prophesize(LongRunningConnectionInterface::class)->reveal(),
+ [],
+ self::PROJECT,
+ self::DATABASE
+ ]);
+ }
+
+ public function testName()
+ {
+ $snippet = $this->snippetFromMethod(Database::class, 'name');
+ $snippet->addLocal('database', $this->database);
+ $res = $snippet->invoke('name');
+ $this->assertEquals(self::DATABASE, $res->returnVal());
+ }
+
+ public function testExists()
+ {
+ $snippet = $this->snippetFromMethod(Database::class, 'exists');
+ $snippet->addLocal('database', $this->database);
+
+ $this->connection->getDatabaseDDL(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['statements' => []]);
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke();
+ $this->assertEquals('Database exists!', $res->output());
+ }
+
+ public function testUpdateDdl()
+ {
+ $snippet = $this->snippetFromMethod(Database::class, 'updateDdl');
+ $snippet->addLocal('database', $this->database);
+
+ $this->connection->updateDatabase(Argument::any())
+ ->shouldBeCalled();
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $snippet->invoke();
+ }
+
+ public function testUpdateDdlBatch()
+ {
+ $snippet = $this->snippetFromMethod(Database::class, 'updateDdlBatch');
+ $snippet->addLocal('database', $this->database);
+
+ $this->connection->updateDatabase(Argument::any())
+ ->shouldBeCalled();
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $snippet->invoke();
+ }
+
+ public function testDrop()
+ {
+ $snippet = $this->snippetFromMethod(Database::class, 'drop');
+ $snippet->addLocal('database', $this->database);
+
+ $this->connection->dropDatabase(Argument::any())
+ ->shouldBeCalled();
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $snippet->invoke();
+ }
+
+ public function testDdl()
+ {
+ $snippet = $this->snippetFromMethod(Database::class, 'ddl');
+ $snippet->addLocal('database', $this->database);
+
+ $stmts = [
+ 'CREATE TABLE TestSuites',
+ 'CREATE TABLE TestCases'
+ ];
+
+ $this->connection->getDatabaseDDL(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'statements' => $stmts
+ ]);
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('statements');
+ $this->assertEquals($stmts, $res->returnVal());
+ }
+
+ public function testIam()
+ {
+ $snippet = $this->snippetFromMethod(Database::class, 'iam');
+ $snippet->addLocal('database', $this->database);
+
+ $res = $snippet->invoke('iam');
+ $this->assertInstanceOf(Iam::class, $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Spanner/InstanceTest.php b/tests/snippets/Spanner/InstanceTest.php
new file mode 100644
index 000000000000..1809ed8d7f38
--- /dev/null
+++ b/tests/snippets/Spanner/InstanceTest.php
@@ -0,0 +1,219 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->instance = \Google\Cloud\Dev\stub(Instance::class, [
+ $this->connection->reveal(),
+ $this->prophesize(SessionPoolInterface::class)->reveal(),
+ $this->prophesize(LongRunningConnectionInterface::class)->reveal(),
+ [],
+ self::PROJECT,
+ self::INSTANCE
+ ]);
+ }
+
+ public function testClass()
+ {
+ $snippet = $this->snippetFromClass(Instance::class);
+ $res = $snippet->invoke('instance');
+ $this->assertInstanceOf(Instance::class, $res->returnVal());
+ $this->assertEquals(self::INSTANCE, $res->returnVal()->name());
+ }
+
+ public function testName()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'name');
+ $snippet->addLocal('instance', $this->instance);
+
+ $res = $snippet->invoke('name');
+ $this->assertEquals(self::INSTANCE, $res->returnVal());
+ }
+
+ public function testInfo()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'info');
+ $snippet->addLocal('instance', $this->instance);
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['nodeCount' => 1]);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke();
+ $this->assertEquals('1', $res->output());
+ }
+
+ public function testExists()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'exists');
+ $snippet->addLocal('instance', $this->instance);
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['foo' => 'bar']);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke();
+ $this->assertEquals('Instance exists!', $res->output());
+ }
+
+ public function testReload()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'reload');
+ $snippet->addLocal('instance', $this->instance);
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn(['nodeCount' => 1]);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('info');
+ $info = $this->instance->info();
+ $this->assertEquals($info, $res->returnVal());
+ }
+
+ public function testState()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'state');
+ $snippet->addLocal('instance', $this->instance);
+ $snippet->addUse(Instance::class);
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn(['state' => Instance::STATE_READY]);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke();
+ $this->assertEquals('Instance is ready!', $res->output());
+ }
+
+ public function testUpdate()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'update');
+ $snippet->addLocal('instance', $this->instance);
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn([
+ 'displayName' => 'foo',
+ 'nodeCount' => 1
+ ]);
+
+ $this->connection->updateInstance(Argument::any())
+ ->shouldBeCalled();
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+ $snippet->invoke();
+ }
+
+ public function testDelete()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'delete');
+ $snippet->addLocal('instance', $this->instance);
+
+ $this->connection->deleteInstance(Argument::any())
+ ->shouldBeCalled();
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+ $snippet->invoke();
+ }
+
+ public function testCreateDatabase()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'createDatabase');
+ $snippet->addLocal('instance', $this->instance);
+
+ $this->connection->createDatabase(Argument::any())
+ ->shouldBeCalled();
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('database');
+ $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal());
+ }
+
+ public function testDatabase()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'database');
+ $snippet->addLocal('instance', $this->instance);
+
+ $res = $snippet->invoke('database');
+ $this->assertInstanceOf(Database::class, $res->returnVal());
+ $this->assertEquals(self::DATABASE, $res->returnVal()->name());
+ }
+
+ public function databases()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'databases');
+ $snippet->addLocal('instance', $this->instance);
+
+ $this->connection->listDatabases(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'databases' => [
+ 'projects/'. self::PROJECT .'/instances/'. self::INSTANCE .'/database/'. self::DATABASE
+ ]
+ ]);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('databases');
+
+ $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(Database::class, $res->returnVal()->current());
+ }
+
+ public function testIam()
+ {
+ $snippet = $this->snippetFromMethod(Instance::class, 'iam');
+ $snippet->addLocal('instance', $this->instance);
+
+ $res = $snippet->invoke('iam');
+ $this->assertInstanceOf(Iam::class, $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Spanner/SpannerClientTest.php b/tests/snippets/Spanner/SpannerClientTest.php
new file mode 100644
index 000000000000..4776185316dc
--- /dev/null
+++ b/tests/snippets/Spanner/SpannerClientTest.php
@@ -0,0 +1,131 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->client = \Google\Cloud\Dev\stub(SpannerClient::class);
+ $this->client->___setProperty('connection', $this->connection->reveal());
+ }
+
+ public function testConfigurations()
+ {
+ $this->connection->listConfigs(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'instanceConfigs' => [
+ ['name' => 'projects/my-awesome-projects/instanceConfigs/Foo'],
+ ['name' => 'projects/my-awesome-projects/instanceConfigs/Bar'],
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $snippet = $this->snippetFromMethod(SpannerClient::class, 'configurations');
+ $snippet->addLocal('spanner', $this->client);
+
+ $res = $snippet->invoke('configurations');
+
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
+ $this->assertInstanceOf(Configuration::class, $res->returnVal()->current());
+ $this->assertEquals('Foo', $res->returnVal()->current()->name());
+ }
+
+ public function testConfiguration()
+ {
+ $configName = 'foo';
+
+ $snippet = $this->snippetFromMethod(SpannerClient::class, 'configuration');
+ $snippet->addLocal('spanner', $this->client);
+ $snippet->addLocal('configurationName', self::CONFIG);
+
+ $res = $snippet->invoke('configuration');
+ $this->assertInstanceOf(Configuration::class, $res->returnVal());
+ $this->assertEquals(self::CONFIG, $res->returnVal()->name());
+ }
+
+ public function testCreateInstance()
+ {
+ $snippet = $this->snippetFromMethod(SpannerClient::class, 'createInstance');
+ $snippet->addLocal('spanner', $this->client);
+ $snippet->addLocal('configuration', $this->client->configuration(self::CONFIG));
+
+ $this->connection->createInstance(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['name' => 'operations/foo']);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('operation');
+ $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal());
+ }
+
+ public function testInstance()
+ {
+ $snippet = $this->snippetFromMethod(SpannerClient::class, 'instance');
+ $snippet->addLocal('spanner', $this->client);
+
+ $res = $snippet->invoke('instance');
+ $this->assertInstanceOf(Instance::class, $res->returnVal());
+ $this->assertEquals(self::INSTANCE, $res->returnVal()->name());
+ }
+
+ public function testInstances()
+ {
+ $snippet = $this->snippetFromMethod(SpannerClient::class, 'instances');
+ $snippet->addLocal('spanner', $this->client);
+
+ $this->connection->listInstances(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'instances' => [
+ ['name' => 'projects/my-awesome-project/instances/'. self::INSTANCE],
+ ['name' => 'projects/my-awesome-project/instances/Bar']
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('instances');
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
+ $this->assertInstanceOf(Instance::class, $res->returnVal()->current());
+ $this->assertEquals(self::INSTANCE, $res->returnVal()->current()->name());
+ }
+}
diff --git a/tests/snippets/Speech/OperationTest.php b/tests/snippets/Speech/OperationTest.php
index 0ac7b04ecb59..400c6d92b44a 100644
--- a/tests/snippets/Speech/OperationTest.php
+++ b/tests/snippets/Speech/OperationTest.php
@@ -47,27 +47,31 @@ public function setUp()
];
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->operation = new \SpeechOperationStub($this->connection->reveal(), $this->opData['name'], $this->opData);
+ $this->operation = \Google\Cloud\Dev\stub(Operation::class, [
+ $this->connection->reveal(),
+ $this->opData['name'],
+ $this->opData
+ ]);
}
- // /**
- // * @expectedException InvalidArgumentException
- // */
- // public function testClass()
- // {
- // $snippet = $this->snippetFromClass(Operation::class);
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testClass()
+ {
+ $snippet = $this->snippetFromClass(Operation::class);
- // $connectionStub = $this->prophesize(ConnectionInterface::class);
+ $connectionStub = $this->prophesize(ConnectionInterface::class);
- // $connectionStub->asyncRecognize(Argument::any())
- // ->willReturn(['name' => 'foo']);
+ $connectionStub->asyncRecognize(Argument::any())
+ ->willReturn(['name' => 'foo']);
- // $snippet->addLocal('connectionStub', $connectionStub->reveal());
+ $snippet->addLocal('connectionStub', $connectionStub->reveal());
- // $snippet->setLine(5, '$audioFileStream = fopen(\'php://temp\', \'r\');');
+ $snippet->replace("__DIR__ . '/audio.flac'", '"php://temp"');
- // $res = $snippet->invoke('operation');
- // }
+ $res = $snippet->invoke('operation');
+ }
public function testIsComplete()
{
@@ -114,7 +118,7 @@ public function testReload()
->shouldBeCalled()
->willReturn($this->opData);
- $this->operation->setConnection($this->connection->reveal());
+ $this->operation->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals(print_r($this->opData['response'], true), $res->output());
diff --git a/tests/snippets/Speech/SpeechClientTest.php b/tests/snippets/Speech/SpeechClientTest.php
index b8d572eca60c..9a1f70ddab95 100644
--- a/tests/snippets/Speech/SpeechClientTest.php
+++ b/tests/snippets/Speech/SpeechClientTest.php
@@ -34,10 +34,10 @@ class SpeechClientTest extends SnippetTestCase
public function setUp()
{
- $this->testFile = "'" . __DIR__ .'/../fixtures/Speech/demo.flac' . "'";
+ $this->testFile = "'" . __DIR__ . '/../fixtures/Speech/demo.flac' . "'";
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->client = new \SpeechClientStub;
- $this->client->setConnection($this->connection->reveal());
+ $this->client = \Google\Cloud\Dev\stub(SpeechClient::class);
+ $this->client->___setProperty('connection', $this->connection->reveal());
}
public function testClass()
@@ -48,14 +48,6 @@ public function testClass()
$this->assertInstanceOf(SpeechClient::class, $res->returnVal());
}
- public function testClassDirectInstantiation()
- {
- $snippet = $this->snippetFromClass(SpeechClient::class, 1);
-
- $res = $snippet->invoke('speech');
- $this->assertInstanceOf(SpeechClient::class, $res->returnVal());
- }
-
public function testRecognize()
{
$snippet = $this->snippetFromMethod(SpeechClient::class, 'recognize', 0);
@@ -77,7 +69,7 @@ public function testRecognize()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals($transcript, $res->output());
@@ -104,7 +96,7 @@ public function testRecognizeWithOptions()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals($transcript, $res->output());
@@ -135,7 +127,7 @@ public function testBeginRecognizeOperation()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals(print_r($results[0]['alternatives'], true), $res->output());
@@ -166,7 +158,7 @@ public function testBeginRecognizeOperationWithOptions()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals(print_r($results[0]['alternatives'], true), $res->output());
diff --git a/tests/snippets/Storage/AclTest.php b/tests/snippets/Storage/AclTest.php
index a81f9ea4f0c8..0a34b035a685 100644
--- a/tests/snippets/Storage/AclTest.php
+++ b/tests/snippets/Storage/AclTest.php
@@ -33,7 +33,11 @@ class AclTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->acl = new \AclStub($this->connection->reveal(), 'bucketAccessControls', []);
+ $this->acl = \Google\Cloud\Dev\stub(Acl::class, [
+ $this->connection->reveal(),
+ 'bucketAccessControls',
+ []
+ ]);
}
public function testClass()
{
@@ -51,7 +55,7 @@ public function testDelete()
$this->connection->deleteAcl(Argument::any())
->shouldBeCalled();
- $this->acl->setConnection($this->connection->reveal());
+ $this->acl->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -65,7 +69,7 @@ public function testGet()
->shouldBeCalled()
->willReturn('foo');
- $this->acl->setConnection($this->connection->reveal());
+ $this->acl->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('res');
$this->assertEquals('foo', $res->returnVal());
@@ -79,7 +83,7 @@ public function testAdd()
$this->connection->insertAcl(Argument::any())
->shouldBecalled();
- $this->acl->setConnection($this->connection->reveal());
+ $this->acl->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -92,7 +96,7 @@ public function testUpdate()
$this->connection->patchAcl(Argument::any())
->shouldBeCalled();
- $this->acl->setConnection($this->connection->reveal());
+ $this->acl->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
}
diff --git a/tests/snippets/Storage/BucketTest.php b/tests/snippets/Storage/BucketTest.php
index c0bd6a6a4994..636e5583075c 100644
--- a/tests/snippets/Storage/BucketTest.php
+++ b/tests/snippets/Storage/BucketTest.php
@@ -18,13 +18,15 @@
namespace Google\Cloud\Tests\Snippets\Storage;
use Google\Cloud\Dev\Snippet\SnippetTestCase;
-use Google\Cloud\Exception\GoogleException;
+use Google\Cloud\Core\Exception\GoogleException;
use Google\Cloud\Storage\Acl;
use Google\Cloud\Storage\Bucket;
use Google\Cloud\Storage\Connection\ConnectionInterface;
+use Google\Cloud\Storage\ObjectIterator;
use Google\Cloud\Storage\StorageObject;
-use Google\Cloud\Upload\MultipartUploader;
-use Google\Cloud\Upload\ResumableUploader;
+use Google\Cloud\Core\Upload\MultipartUploader;
+use Google\Cloud\Core\Upload\ResumableUploader;
+use Google\Cloud\Core\Upload\StreamableUploader;
use Prophecy\Argument;
/**
@@ -40,7 +42,10 @@ class BucketTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->bucket = new \BucketStub($this->connection->reveal(), self::BUCKET);
+ $this->bucket = \Google\Cloud\Dev\stub(Bucket::class, [
+ $this->connection->reveal(),
+ self::BUCKET
+ ]);
}
public function testClass()
@@ -79,7 +84,7 @@ public function testExists()
$this->connection->getBucket(Argument::any())
->shouldBeCalled();
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Bucket exists!', $res->output());
@@ -103,7 +108,7 @@ public function testUpload()
->shouldBeCalled()
->willReturn($uploader->reveal());
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('object');
$this->assertInstanceOf(StorageObject::class, $res->returnVal());
@@ -130,7 +135,7 @@ public function testUploadResumableUploader()
->shouldBeCalled()
->willReturn($uploader->reveal());
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('object');
$this->assertInstanceOf(StorageObject::class, $res->returnVal());
@@ -157,7 +162,7 @@ public function testUploadEncryption()
->shouldBeCalled()
->willReturn($uploader->reveal());
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('object');
$this->assertInstanceOf(StorageObject::class, $res->returnVal());
@@ -191,11 +196,31 @@ public function testGetResumableUploader()
->shouldBeCalled()
->willReturn($uploader->reveal());
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('object');
}
+ public function testGetStreamableUploader()
+ {
+ $snippet = $this->snippetFromMethod(Bucket::class, 'getStreamableUploader');
+ $snippet->addLocal('bucket', $this->bucket);
+ $snippet->addUse(GoogleException::class);
+ $snippet->replace("data.txt", 'php://temp');
+
+ $uploader = $this->prophesize(StreamableUploader::class);
+ $uploader->upload()
+ ->shouldBeCalledTimes(1);
+
+ $this->connection->insertObject(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn($uploader->reveal());
+
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke();
+ }
+
public function testObject()
{
$snippet = $this->snippetFromMethod(Bucket::class, 'object');
@@ -214,15 +239,21 @@ public function testObjects()
->shouldBeCalled()
->willReturn([
'items' => [
- ['name' => 'object 1'],
- ['name' => 'object 2']
+ [
+ 'name' => 'object 1',
+ 'generation' => 'abc'
+ ],
+ [
+ 'name' => 'object 2',
+ 'generation' => 'def'
+ ]
]
]);
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('objects');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ObjectIterator::class, $res->returnVal());
$this->assertEquals('object 1', explode("\n", $res->output())[0]);
$this->assertEquals('object 2', explode("\n", $res->output())[1]);
}
@@ -235,7 +266,7 @@ public function testDelete()
$this->connection->deleteBucket(Argument::any())
->shouldBeCalled();
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -254,7 +285,7 @@ public function testUpdate()
->shouldBeCalled()
->willReturn('foo');
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
}
@@ -271,7 +302,7 @@ public function testCompose()
'generation' => 'foo'
]);
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('singleObject');
$this->assertInstanceOf(StorageObject::class, $res->returnVal());
@@ -290,7 +321,7 @@ public function testComposeWithObjects()
'generation' => 'foo'
]);
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('singleObject');
$this->assertInstanceOf(StorageObject::class, $res->returnVal());
@@ -309,7 +340,7 @@ public function testInfo()
'location' => $loc
]);
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals($loc, $res->output());
@@ -327,7 +358,7 @@ public function testReload()
'location' => $loc
]);
- $this->bucket->setConnection($this->connection->reveal());
+ $this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals($loc, $res->output());
diff --git a/tests/snippets/Storage/StorageClientTest.php b/tests/snippets/Storage/StorageClientTest.php
index da195fc1d820..b50ab1a60cb4 100644
--- a/tests/snippets/Storage/StorageClientTest.php
+++ b/tests/snippets/Storage/StorageClientTest.php
@@ -21,6 +21,7 @@
use Google\Cloud\Storage\Bucket;
use Google\Cloud\Storage\Connection\ConnectionInterface;
use Google\Cloud\Storage\StorageClient;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Prophecy\Argument;
/**
@@ -36,8 +37,8 @@ class StorageClientTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->client = new \StorageClientStub;
- $this->client->setConnection($this->connection->reveal());
+ $this->client = \Google\Cloud\Dev\stub(StorageClient::class);
+ $this->client->___setProperty('connection', $this->connection->reveal());
}
public function testClass()
@@ -47,13 +48,6 @@ public function testClass()
$this->assertInstanceOf(StorageClient::class, $res->returnVal());
}
- public function testClassDirectInstantiation()
- {
- $snippet = $this->snippetFromClass(StorageClient::class, 1);
- $res = $snippet->invoke('storage');
- $this->assertInstanceOf(StorageClient::class, $res->returnVal());
- }
-
public function testBucket()
{
$snippet = $this->snippetFromMethod(StorageClient::class, 'bucket');
@@ -77,10 +71,10 @@ public function testBuckets()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('buckets');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$buckets = iterator_to_array($res->returnVal());
$this->assertEquals('album 1', $buckets[0]->name());
@@ -101,10 +95,10 @@ public function testBucketsWithPrefix()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('buckets');
- $this->assertInstanceOf(\Generator::class, $res->returnVal());
+ $this->assertInstanceOf(ItemIterator::class, $res->returnVal());
$this->assertEquals('album 1', explode("\n", $res->output())[0]);
$this->assertEquals('album 2', explode("\n", $res->output())[1]);
}
@@ -118,7 +112,22 @@ public function testCreateBucket()
->shouldBeCalled()
->willReturn([]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('bucket');
+ $this->assertInstanceOf(Bucket::class, $res->returnVal());
+ }
+
+ public function testCreateBucketWithLogging()
+ {
+ $snippet = $this->snippetFromMethod(StorageClient::class, 'createBucket', 1);
+ $snippet->addLocal('storage', $this->client);
+
+ $this->connection->insertBucket(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('bucket');
$this->assertInstanceOf(Bucket::class, $res->returnVal());
diff --git a/tests/snippets/Storage/StorageObjectTest.php b/tests/snippets/Storage/StorageObjectTest.php
index 541822622455..475ee81e9baa 100644
--- a/tests/snippets/Storage/StorageObjectTest.php
+++ b/tests/snippets/Storage/StorageObjectTest.php
@@ -19,7 +19,9 @@
use Google\Cloud\Dev\Snippet\SnippetTestCase;
use Google\Cloud\Storage\Acl;
+use Google\Cloud\Storage\Bucket;
use Google\Cloud\Storage\Connection\ConnectionInterface;
+use Google\Cloud\Storage\StorageClient;
use Google\Cloud\Storage\StorageObject;
use Prophecy\Argument;
use Psr\Http\Message\StreamInterface;
@@ -38,11 +40,11 @@ class StorageObjectTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->object = new \StorageObjectStub(
+ $this->object = \Google\Cloud\Dev\stub(StorageObject::class, [
$this->connection->reveal(),
self::OBJECT,
self::BUCKET
- );
+ ]);
}
public function testClass()
@@ -70,7 +72,7 @@ public function testExists()
->shouldBeCalled()
->willReturn([]);
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('Object exists!', $res->output());
@@ -84,7 +86,7 @@ public function testDelete()
$this->connection->deleteObject(Argument::any())
->shouldBeCalled();
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -97,7 +99,7 @@ public function testUpdate()
$this->connection->patchObject(Argument::any())
->shouldBeCalled();
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$snippet->invoke();
}
@@ -115,7 +117,34 @@ public function testCopy()
'generation' => 'foo'
]);
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('copiedObject');
+ $this->assertInstanceOf(StorageObject::class, $res->returnVal());
+ }
+
+ public function testCopyToBucket()
+ {
+ $bucket = $this->prophesize(Bucket::class);
+ $bucket->name()->willReturn('foo');
+
+ $storage = $this->prophesize(StorageClient::class);
+ $storage->bucket(Argument::any())
+ ->willReturn($bucket->reveal());
+
+ $snippet = $this->snippetFromMethod(StorageObject::class, 'copy', 1);
+ $snippet->addLocal('object', $this->object);
+ $snippet->addLocal('storage', $storage->reveal());
+
+ $this->connection->copyObject(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'name' => 'New Object',
+ 'bucket' => self::BUCKET,
+ 'generation' => 'foo'
+ ]);
+
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('copiedObject');
$this->assertInstanceOf(StorageObject::class, $res->returnVal());
@@ -125,6 +154,56 @@ public function testRewrite()
{
$snippet = $this->snippetFromMethod(StorageObject::class, 'rewrite');
$snippet->addLocal('object', $this->object);
+
+ $this->connection->rewriteObject(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'resource' => [
+ 'name' => self::OBJECT,
+ 'bucket' => self::BUCKET,
+ 'generation' => 'foo'
+ ]
+ ]);
+
+ $this->object->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('rewrittenObject');
+ $this->assertInstanceOf(StorageObject::class, $res->returnVal());
+ }
+
+ public function testRewriteNewObjectName()
+ {
+ $bucket = $this->prophesize(Bucket::class);
+ $bucket->name()->willReturn('foo');
+
+ $storage = $this->prophesize(StorageClient::class);
+ $storage->bucket(Argument::any())
+ ->willReturn($bucket->reveal());
+
+ $snippet = $this->snippetFromMethod(StorageObject::class, 'rewrite', 1);
+ $snippet->addLocal('storage', $storage->reveal());
+ $snippet->addLocal('object', $this->object);
+
+ $this->connection->rewriteObject(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'resource' => [
+ 'name' => self::OBJECT,
+ 'bucket' => self::BUCKET,
+ 'generation' => 'foo'
+ ]
+ ]);
+
+ $this->object->___setProperty('connection', $this->connection->reveal());
+
+ $res = $snippet->invoke('rewrittenObject');
+ $this->assertInstanceOf(StorageObject::class, $res->returnVal());
+ }
+
+ public function testRewriteNewKey()
+ {
+ $snippet = $this->snippetFromMethod(StorageObject::class, 'rewrite', 2);
+ $snippet->addLocal('object', $this->object);
$snippet->replace("file_get_contents(__DIR__ . '/key.txt')", "'testKeyData'");
$this->connection->rewriteObject(Argument::any())
@@ -137,7 +216,7 @@ public function testRewrite()
]
]);
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('rewrittenObject');
$this->assertInstanceOf(StorageObject::class, $res->returnVal());
@@ -159,7 +238,7 @@ public function testRename()
$this->connection->deleteObject(Argument::any())
->shouldBeCalled();
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('object2.txt', $res->output());
@@ -174,7 +253,7 @@ public function testDownloadAsString()
->shouldBeCalled()
->willReturn(\GuzzleHttp\Psr7\stream_for('test'));
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('test', $res->output());
@@ -190,7 +269,7 @@ public function testDownloadToFile()
->shouldBeCalled()
->willReturn(\GuzzleHttp\Psr7\stream_for('test'));
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('stream');
@@ -206,7 +285,7 @@ public function testDownloadAsStream()
->shouldBeCalled()
->willReturn(\GuzzleHttp\Psr7\stream_for('test'));
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
@@ -227,7 +306,7 @@ public function testInfo()
'location' => 'right behind you!'
]);
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('1', $res->output());
@@ -247,7 +326,7 @@ public function testReload()
'location' => 'right behind you!'
]);
- $this->object->setConnection($this->connection->reveal());
+ $this->object->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('right behind you!', $res->output());
@@ -270,4 +349,14 @@ public function testIdentity()
$res = $snippet->invoke();
$this->assertEquals(self::OBJECT, $res->output());
}
+
+ public function testGcsUri()
+ {
+ $snippet = $this->snippetFromMethod(StorageObject::class, 'gcsUri');
+ $snippet->addLocal('object', $this->object);
+
+ $res = $snippet->invoke();
+ $expectedOutput = sprintf('gs://%s/%s', self::BUCKET, self::OBJECT);
+ $this->assertEquals($expectedOutput, $res->output());
+ }
}
diff --git a/tests/snippets/Translate/TranslateClientTest.php b/tests/snippets/Translate/TranslateClientTest.php
index 05190e750867..00ba0252d86f 100644
--- a/tests/snippets/Translate/TranslateClientTest.php
+++ b/tests/snippets/Translate/TranslateClientTest.php
@@ -33,11 +33,11 @@ class TranslateClientTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->client = new \TranslateClientStub;
- $this->client->setConnection($this->connection->reveal());
+ $this->client = \Google\Cloud\Dev\stub(TranslateClient::class);
+ $this->client->___setProperty('connection', $this->connection->reveal());
}
- public function testTrue()
+ public function testClass()
{
$snippet = $this->snippetFromClass(TranslateClient::class);
$res = $snippet->invoke('translate');
@@ -63,7 +63,7 @@ public function testTranslate()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('foobar', $res->output());
@@ -87,7 +87,7 @@ public function testTranslateBatch()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('foobar', $res->output());
@@ -113,7 +113,7 @@ public function testDetectLanguage()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('en', $res->output());
@@ -139,7 +139,7 @@ public function testDetectLanguageBatch()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('en', $res->output());
@@ -163,7 +163,7 @@ public function testLanguages()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('en', $res->output());
@@ -187,7 +187,7 @@ public function testLocalizedLanguages()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke();
$this->assertEquals('en', $res->output());
diff --git a/tests/snippets/Vision/Annotation/CropHintTest.php b/tests/snippets/Vision/Annotation/CropHintTest.php
new file mode 100644
index 000000000000..7a9f60d7fae4
--- /dev/null
+++ b/tests/snippets/Vision/Annotation/CropHintTest.php
@@ -0,0 +1,102 @@
+info = [
+ 'boundingPoly' => ['foo' => 'bar'],
+ 'confidence' => 0.4,
+ 'importanceFraction' => 0.1
+ ];
+
+ $this->hint = new CropHint($this->info);
+ }
+
+ public function testClass()
+ {
+ $connectionStub = $this->prophesize(ConnectionInterface::class);
+
+ $connectionStub->annotate(Argument::any())
+ ->willReturn([
+ 'responses' => [
+ [
+ 'cropHintsAnnotation' => [
+ 'cropHints' => [[]]
+ ]
+ ]
+ ]
+ ]);
+
+ $snippet = $this->snippetFromClass(CropHint::class);
+ $snippet->addLocal('connectionStub', $connectionStub->reveal());
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
+ $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
+ $property = $reflection->getProperty(\'connection\');
+ $property->setAccessible(true);
+ $property->setValue($vision, $connectionStub);
+ $property->setAccessible(false);'
+ );
+
+ $res = $snippet->invoke('hint');
+ $this->assertInstanceOf(CropHint::class, $res->returnVal());
+ }
+
+ public function testBoundingPoly()
+ {
+ $snippet = $this->snippetFromMagicMethod(CropHint::class, 'boundingPoly');
+ $snippet->addLocal('hint', $this->hint);
+
+ $res = $snippet->invoke('poly');
+ $this->assertEquals($this->info['boundingPoly'], $res->returnVal());
+ }
+
+ public function testConfidence()
+ {
+ $snippet = $this->snippetFromMagicMethod(CropHint::class, 'confidence');
+ $snippet->addLocal('hint', $this->hint);
+
+ $res = $snippet->invoke('confidence');
+ $this->assertEquals($this->info['confidence'], $res->returnVal());
+ }
+
+ public function testImportanceFraction()
+ {
+ $snippet = $this->snippetFromMagicMethod(CropHint::class, 'importanceFraction');
+ $snippet->addLocal('hint', $this->hint);
+
+ $res = $snippet->invoke('importance');
+ $this->assertEquals($this->info['importanceFraction'], $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Vision/Annotation/DocumentTest.php b/tests/snippets/Vision/Annotation/DocumentTest.php
new file mode 100644
index 000000000000..d34f67997519
--- /dev/null
+++ b/tests/snippets/Vision/Annotation/DocumentTest.php
@@ -0,0 +1,98 @@
+info = [
+ 'pages' => [['foo' => 'bar']],
+ 'text' => 'hello world'
+ ];
+ $this->document = new Document($this->info);
+ }
+
+ public function testClass()
+ {
+ $connectionStub = $this->prophesize(ConnectionInterface::class);
+
+ $connectionStub->annotate(Argument::any())
+ ->willReturn([
+ 'responses' => [
+ [
+ 'fullTextAnnotation' => [[]]
+ ]
+ ]
+ ]);
+
+ $snippet = $this->snippetFromClass(Document::class);
+ $snippet->addLocal('connectionStub', $connectionStub->reveal());
+ $snippet->replace(
+ "__DIR__ . '/assets/the-constitution.jpg'",
+ "'php://temp'"
+ );
+ $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
+ $property = $reflection->getProperty(\'connection\');
+ $property->setAccessible(true);
+ $property->setValue($vision, $connectionStub);
+ $property->setAccessible(false);'
+ );
+
+ $res = $snippet->invoke('document');
+ $this->assertInstanceOf(Document::class, $res->returnVal());
+ }
+
+ public function testPages()
+ {
+ $snippet = $this->snippetFromMagicMethod(Document::class, 'pages');
+ $snippet->addLocal('document', $this->document);
+
+ $res = $snippet->invoke('pages');
+ $this->assertEquals($this->info['pages'], $res->returnVal());
+ }
+
+ public function testText()
+ {
+ $snippet = $this->snippetFromMagicMethod(Document::class, 'text');
+ $snippet->addLocal('document', $this->document);
+
+ $res = $snippet->invoke('text');
+ $this->assertEquals($this->info['text'], $res->returnVal());
+ }
+
+ public function testInfo()
+ {
+ $snippet = $this->snippetFromMagicMethod(Document::class, 'info');
+ $snippet->addLocal('document', $this->document);
+
+ $res = $snippet->invoke('info');
+ $this->assertEquals($this->info, $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Vision/Annotation/EntityTest.php b/tests/snippets/Vision/Annotation/EntityTest.php
index 2e0ff26232e1..d990ee8c27ce 100644
--- a/tests/snippets/Vision/Annotation/EntityTest.php
+++ b/tests/snippets/Vision/Annotation/EntityTest.php
@@ -62,7 +62,10 @@ public function testClass()
$snippet = $this->snippetFromClass(Entity::class);
$snippet->addLocal('connectionStub', $connectionStub->reveal());
- $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
$property = $reflection->getProperty(\'connection\');
$property->setAccessible(true);
@@ -74,6 +77,13 @@ public function testClass()
$this->assertInstanceOf(Entity::class, $res->returnVal());
}
+ public function testInfo()
+ {
+ $snippet = $this->snippetFromMagicMethod(Entity::class, 'info');
+ $snippet->addLocal('text', $this->entity);
+ $this->assertEquals($this->entityData, $snippet->invoke('info')->returnVal());
+ }
+
public function testMid()
{
$snippet = $this->snippetFromMagicMethod(Entity::class, 'mid');
@@ -154,5 +164,4 @@ public function testProperties()
$res = $snippet->invoke();
$this->assertEquals($this->entityData['properties'], $res->output());
}
-
}
diff --git a/tests/snippets/Vision/Annotation/Face/LandmarksTest.php b/tests/snippets/Vision/Annotation/Face/LandmarksTest.php
index b70e41fed9ec..858a6a810f6e 100644
--- a/tests/snippets/Vision/Annotation/Face/LandmarksTest.php
+++ b/tests/snippets/Vision/Annotation/Face/LandmarksTest.php
@@ -98,7 +98,10 @@ public function testClass()
$snippet = $this->snippetFromClass(Landmarks::class);
$snippet->addLocal('connectionStub', $connectionStub->reveal());
- $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
$property = $reflection->getProperty(\'connection\');
$property->setAccessible(true);
@@ -153,7 +156,7 @@ public function testLeftEyeBoundaries()
public function testLeftEyeBrow()
{
- $snippet = $this->snippetFromMethod(Landmarks::class, 'leftEyeBrow');
+ $snippet = $this->snippetFromMethod(Landmarks::class, 'leftEyebrow');
$snippet->addLocal('landmarks', $this->landmarks);
$res = $snippet->invoke();
@@ -198,7 +201,7 @@ public function testRightEyeBoundaries()
public function testRightEyeBrow()
{
- $snippet = $this->snippetFromMethod(Landmarks::class, 'rightEyeBrow');
+ $snippet = $this->snippetFromMethod(Landmarks::class, 'rightEyebrow');
$snippet->addLocal('landmarks', $this->landmarks);
$res = $snippet->invoke();
diff --git a/tests/snippets/Vision/Annotation/FaceTest.php b/tests/snippets/Vision/Annotation/FaceTest.php
index 9e9ade0a923a..81a9127a5a83 100644
--- a/tests/snippets/Vision/Annotation/FaceTest.php
+++ b/tests/snippets/Vision/Annotation/FaceTest.php
@@ -41,13 +41,13 @@ public function setUp()
"tiltAngle" => 'testtiltAngle',
"detectionConfidence" => 'testdetectionConfidence',
"landmarkingConfidence" => 'testlandmarkingConfidence',
- "joyLikelihood" => 'testjoyLikelihood',
- "sorrowLikelihood" => 'testsorrowLikelihood',
- "angerLikelihood" => 'testangerLikelihood',
- "surpriseLikelihood" => 'testsurpriseLikelihood',
- "underExposedLikelihood" => 'testunderExposedLikelihood',
- "blurredLikelihood" => 'testblurredLikelihood',
- "headwearLikelihood" => 'testheadwearLikelihood',
+ "joyLikelihood" => 'VERY_LIKELY',
+ "sorrowLikelihood" => 'VERY_LIKELY',
+ "angerLikelihood" => 'VERY_LIKELY',
+ "surpriseLikelihood" => 'VERY_LIKELY',
+ "underExposedLikelihood" => 'VERY_LIKELY',
+ "blurredLikelihood" => 'VERY_LIKELY',
+ "headwearLikelihood" => 'VERY_LIKELY',
];
$this->face = new Face($this->faceData);
@@ -74,7 +74,10 @@ public function testClass()
$snippet = $this->snippetFromClass(Face::class);
$snippet->addLocal('connectionStub', $connectionStub->reveal());
- $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
$property = $reflection->getProperty(\'connection\');
$property->setAccessible(true);
@@ -83,6 +86,13 @@ public function testClass()
);
}
+ public function testInfo()
+ {
+ $snippet = $this->snippetFromMagicMethod(Face::class, 'info');
+ $snippet->addLocal('face', $this->face);
+ $this->assertEquals($this->faceData, $snippet->invoke('info')->returnVal());
+ }
+
public function testLandmarks()
{
$snippet = $this->snippetFromMagicMethod(Face::class, 'landmarks');
@@ -225,4 +235,28 @@ public function testHeadwearlikelihood()
$this->assertEquals($this->faceData['headwearLikelihood'], $res->output());
}
+ /**
+ * @dataProvider boolTests
+ */
+ public function testFaceBoolTests($method, $output)
+ {
+ $snippet = $this->snippetFromMethod(Face::class, $method);
+ $snippet->addLocal('face', $this->face);
+
+ $res = $snippet->invoke();
+ $this->assertEquals($output, $res->output());
+ }
+
+ public function boolTests()
+ {
+ return [
+ ['isJoyful', 'Face is Joyful'],
+ ['isSorrowful', 'Face is Sorrowful'],
+ ['isAngry', 'Face is Angry'],
+ ['isSurprised', 'Face is Surprised'],
+ ['isUnderExposed', 'Face is Under Exposed'],
+ ['isBlurred', 'Face is Blurred'],
+ ['hasHeadwear', 'Face has Headwear']
+ ];
+ }
}
diff --git a/tests/snippets/Vision/Annotation/ImagePropertiesTest.php b/tests/snippets/Vision/Annotation/ImagePropertiesTest.php
index 03ea867f8f86..62c8c8f82f64 100644
--- a/tests/snippets/Vision/Annotation/ImagePropertiesTest.php
+++ b/tests/snippets/Vision/Annotation/ImagePropertiesTest.php
@@ -53,7 +53,10 @@ public function testClass()
$snippet = $this->snippetFromClass(ImageProperties::class);
$snippet->addLocal('connectionStub', $connectionStub->reveal());
- $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
$property = $reflection->getProperty(\'connection\');
$property->setAccessible(true);
diff --git a/tests/snippets/Vision/Annotation/SafeSearchTest.php b/tests/snippets/Vision/Annotation/SafeSearchTest.php
index ac04eddb928f..b0af9e9a5c30 100644
--- a/tests/snippets/Vision/Annotation/SafeSearchTest.php
+++ b/tests/snippets/Vision/Annotation/SafeSearchTest.php
@@ -58,7 +58,10 @@ public function testClass()
$snippet = $this->snippetFromClass(SafeSearch::class);
$snippet->addLocal('connectionStub', $connectionStub->reveal());
- $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
$property = $reflection->getProperty(\'connection\');
$property->setAccessible(true);
@@ -114,6 +117,7 @@ public function testIsAdult()
$res = $snippet->invoke();
$this->assertEquals(sprintf('Image contains %s content.', 'adult'), $res->output());
}
+
public function testIsSpoof()
{
$snippet = $this->snippetFromMethod(SafeSearch::class, 'isSpoof');
@@ -122,6 +126,7 @@ public function testIsSpoof()
$res = $snippet->invoke();
$this->assertEquals(sprintf('Image contains %s content.', 'spoofed'), $res->output());
}
+
public function testIsMedical()
{
$snippet = $this->snippetFromMethod(SafeSearch::class, 'isMedical');
@@ -130,6 +135,7 @@ public function testIsMedical()
$res = $snippet->invoke();
$this->assertEquals(sprintf('Image contains %s content.', 'medical'), $res->output());
}
+
public function testIsViolent()
{
$snippet = $this->snippetFromMethod(SafeSearch::class, 'isViolent');
@@ -138,4 +144,13 @@ public function testIsViolent()
$res = $snippet->invoke();
$this->assertEquals(sprintf('Image contains %s content.', 'violent'), $res->output());
}
+
+ public function testInfo()
+ {
+ $snippet = $this->snippetFromMagicMethod(SafeSearch::class, 'info');
+ $snippet->addLocal('safeSearch', $this->ss);
+
+ $res = $snippet->invoke('info');
+ $this->assertEquals($this->ssData, $res->returnVal());
+ }
}
diff --git a/tests/snippets/Vision/Annotation/Web/WebEntityTest.php b/tests/snippets/Vision/Annotation/Web/WebEntityTest.php
new file mode 100644
index 000000000000..62009e184dcb
--- /dev/null
+++ b/tests/snippets/Vision/Annotation/Web/WebEntityTest.php
@@ -0,0 +1,103 @@
+info = [
+ 'entityId' => 'foo',
+ 'score' => 0.1,
+ 'description' => 'bar'
+ ];
+ $this->entity = new WebEntity($this->info);
+ }
+
+ public function testClass()
+ {
+ $connectionStub = $this->prophesize(ConnectionInterface::class);
+
+ $connectionStub->annotate(Argument::any())
+ ->willReturn([
+ 'responses' => [
+ [
+ 'webDetection' => [
+ 'webEntities' => [
+ []
+ ]
+ ]
+ ]
+ ]
+ ]);
+
+ $snippet = $this->snippetFromClass(WebEntity::class);
+ $snippet->addLocal('connectionStub', $connectionStub->reveal());
+ $snippet->replace(
+ "__DIR__ . '/assets/eiffel-tower.jpg'",
+ "'php://temp'"
+ );
+ $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
+ $property = $reflection->getProperty(\'connection\');
+ $property->setAccessible(true);
+ $property->setValue($vision, $connectionStub);
+ $property->setAccessible(false);'
+ );
+
+ $res = $snippet->invoke('firstEntity');
+ $this->assertInstanceOf(WebEntity::class, $res->returnVal());
+ }
+
+ public function testEntityId()
+ {
+ $snippet = $this->snippetFromMagicMethod(WebEntity::class, 'entityId');
+ $snippet->addLocal('entity', $this->entity);
+
+ $res = $snippet->invoke('id');
+ $this->assertEquals($this->info['entityId'], $res->returnVal());
+ }
+
+ public function testScore()
+ {
+ $snippet = $this->snippetFromMagicMethod(WebEntity::class, 'score');
+ $snippet->addLocal('entity', $this->entity);
+
+ $res = $snippet->invoke('score');
+ $this->assertEquals($this->info['score'], $res->returnVal());
+ }
+
+ public function testDescription()
+ {
+ $snippet = $this->snippetFromMagicMethod(WebEntity::class, 'description');
+ $snippet->addLocal('entity', $this->entity);
+
+ $res = $snippet->invoke('description');
+ $this->assertEquals($this->info['description'], $res->returnVal());
+ }
+}
diff --git a/tests/snippets/Vision/Annotation/Web/WebImageTest.php b/tests/snippets/Vision/Annotation/Web/WebImageTest.php
new file mode 100644
index 000000000000..b8ced484b27c
--- /dev/null
+++ b/tests/snippets/Vision/Annotation/Web/WebImageTest.php
@@ -0,0 +1,93 @@
+info = [
+ 'url' => 'http://foo.bar/image.jpg',
+ 'score' => 0.1,
+ ];
+ $this->image = new WebImage($this->info);
+ }
+
+ public function testClass()
+ {
+ $connectionStub = $this->prophesize(ConnectionInterface::class);
+
+ $connectionStub->annotate(Argument::any())
+ ->willReturn([
+ 'responses' => [
+ [
+ 'webDetection' => [
+ 'fullMatchingImages' => [
+ []
+ ]
+ ]
+ ]
+ ]
+ ]);
+
+ $snippet = $this->snippetFromClass(WebImage::class);
+ $snippet->addLocal('connectionStub', $connectionStub->reveal());
+ $snippet->replace(
+ "__DIR__ . '/assets/eiffel-tower.jpg'",
+ "'php://temp'"
+ );
+ $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
+ $property = $reflection->getProperty(\'connection\');
+ $property->setAccessible(true);
+ $property->setValue($vision, $connectionStub);
+ $property->setAccessible(false);'
+ );
+
+ $res = $snippet->invoke('firstImage');
+ $this->assertInstanceOf(WebImage::class, $res->returnVal());
+ }
+
+ public function testurl()
+ {
+ $snippet = $this->snippetFromMagicMethod(WebImage::class, 'url');
+ $snippet->addLocal('image', $this->image);
+
+ $res = $snippet->invoke('url');
+ $this->assertEquals($this->info['url'], $this->image->url());
+ }
+
+ public function testscore()
+ {
+ $snippet = $this->snippetFromMagicMethod(WebImage::class, 'score');
+ $snippet->addLocal('image', $this->image);
+
+ $res = $snippet->invoke('score');
+ $this->assertEquals($this->info['score'], $this->image->score());
+ }
+}
diff --git a/tests/snippets/Vision/Annotation/Web/WebPageTest.php b/tests/snippets/Vision/Annotation/Web/WebPageTest.php
new file mode 100644
index 000000000000..5e74eafcbe7e
--- /dev/null
+++ b/tests/snippets/Vision/Annotation/Web/WebPageTest.php
@@ -0,0 +1,93 @@
+info = [
+ 'url' => 'http://foo.bar/image.jpg',
+ 'score' => 0.1,
+ ];
+ $this->image = new WebPage($this->info);
+ }
+
+ public function testClass()
+ {
+ $connectionStub = $this->prophesize(ConnectionInterface::class);
+
+ $connectionStub->annotate(Argument::any())
+ ->willReturn([
+ 'responses' => [
+ [
+ 'webDetection' => [
+ 'pagesWithMatchingImages' => [
+ []
+ ]
+ ]
+ ]
+ ]
+ ]);
+
+ $snippet = $this->snippetFromClass(WebPage::class);
+ $snippet->addLocal('connectionStub', $connectionStub->reveal());
+ $snippet->replace(
+ "__DIR__ . '/assets/eiffel-tower.jpg'",
+ "'php://temp'"
+ );
+ $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
+ $property = $reflection->getProperty(\'connection\');
+ $property->setAccessible(true);
+ $property->setValue($vision, $connectionStub);
+ $property->setAccessible(false);'
+ );
+
+ $res = $snippet->invoke('firstPage');
+ $this->assertInstanceOf(WebPage::class, $res->returnVal());
+ }
+
+ public function testurl()
+ {
+ $snippet = $this->snippetFromMagicMethod(WebPage::class, 'url');
+ $snippet->addLocal('image', $this->image);
+
+ $res = $snippet->invoke('url');
+ $this->assertEquals($this->info['url'], $this->image->url());
+ }
+
+ public function testscore()
+ {
+ $snippet = $this->snippetFromMagicMethod(WebPage::class, 'score');
+ $snippet->addLocal('image', $this->image);
+
+ $res = $snippet->invoke('score');
+ $this->assertEquals($this->info['score'], $this->image->score());
+ }
+}
diff --git a/tests/snippets/Vision/Annotation/WebTest.php b/tests/snippets/Vision/Annotation/WebTest.php
new file mode 100644
index 000000000000..38f849b2a8c3
--- /dev/null
+++ b/tests/snippets/Vision/Annotation/WebTest.php
@@ -0,0 +1,120 @@
+info = [
+ 'webEntities' => [
+ []
+ ],
+ 'fullMatchingImages' => [
+ []
+ ],
+ 'partialMatchingImages' => [
+ []
+ ],
+ 'pagesWithMatchingImages' => [
+ []
+ ]
+ ];
+ $this->web = new Web($this->info);
+ }
+
+ public function testClass()
+ {
+ $connectionStub = $this->prophesize(ConnectionInterface::class);
+
+ $connectionStub->annotate(Argument::any())
+ ->willReturn([
+ 'responses' => [
+ [
+ 'webDetection' => []
+ ]
+ ]
+ ]);
+
+ $snippet = $this->snippetFromClass(Web::class);
+ $snippet->addLocal('connectionStub', $connectionStub->reveal());
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
+ $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision);
+ $property = $reflection->getProperty(\'connection\');
+ $property->setAccessible(true);
+ $property->setValue($vision, $connectionStub);
+ $property->setAccessible(false);'
+ );
+
+ $res = $snippet->invoke('web');
+ $this->assertInstanceOf(Web::class, $res->returnVal());
+ }
+
+ public function testEntities()
+ {
+ $snippet = $this->snippetFromMethod(Web::class, 'entities');
+ $snippet->addLocal('web', $this->web);
+
+ $res = $snippet->invoke('entities');
+ $this->assertInstanceOf(WebEntity::class, $res->returnVal()[0]);
+ }
+
+ public function testMatchingImages()
+ {
+ $snippet = $this->snippetFromMethod(Web::class, 'matchingImages');
+ $snippet->addLocal('web', $this->web);
+
+ $res = $snippet->invoke('images');
+ $this->assertInstanceOf(WebImage::class, $res->returnVal()[0]);
+ }
+
+ public function testPartialMatchingImages()
+ {
+ $snippet = $this->snippetFromMethod(Web::class, 'partialMatchingImages');
+ $snippet->addLocal('web', $this->web);
+
+ $res = $snippet->invoke('images');
+ $this->assertInstanceOf(WebImage::class, $res->returnVal()[0]);
+ }
+
+ public function testPages()
+ {
+ $snippet = $this->snippetFromMethod(Web::class, 'pages');
+ $snippet->addLocal('web', $this->web);
+
+ $res = $snippet->invoke('pages');
+ $this->assertInstanceOf(WebPage::class, $res->returnVal()[0]);
+ }
+}
diff --git a/tests/snippets/Vision/AnnotationTest.php b/tests/snippets/Vision/AnnotationTest.php
index 6412667a4dca..16f9dd158454 100644
--- a/tests/snippets/Vision/AnnotationTest.php
+++ b/tests/snippets/Vision/AnnotationTest.php
@@ -19,10 +19,13 @@
use Google\Cloud\Dev\Snippet\SnippetTestCase;
use Google\Cloud\Vision\Annotation;
+use Google\Cloud\Vision\Annotation\CropHint;
+use Google\Cloud\Vision\Annotation\Document;
use Google\Cloud\Vision\Annotation\Entity;
use Google\Cloud\Vision\Annotation\Face;
use Google\Cloud\Vision\Annotation\ImageProperties;
use Google\Cloud\Vision\Annotation\SafeSearch;
+use Google\Cloud\Vision\Annotation\Web;
use Google\Cloud\Vision\Connection\ConnectionInterface;
use Prophecy\Argument;
@@ -35,8 +38,8 @@ public function testClass()
{
$snippet = $this->snippetFromClass(Annotation::class);
$snippet->replace(
- '__DIR__ .\'/assets/family-photo.jpg\'',
- '\'php://temp\''
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
);
$connectionStub = $this->prophesize(ConnectionInterface::class);
@@ -151,6 +154,42 @@ public function testImageProperties()
$this->assertInstanceOf(ImageProperties::class, $res->returnVal());
}
+ public function testFullText()
+ {
+ $ft = ['foo' => 'bar'];
+ $snippet = $this->snippetFromMethod(Annotation::class, 'fullText');
+ $snippet->addLocal('annotation', new Annotation([
+ 'fullTextAnnotation' => $ft
+ ]));
+
+ $res = $snippet->invoke('fullText');
+ $this->assertInstanceOf(Document::class, $res->returnVal());
+ }
+
+ public function testCropHints()
+ {
+ $snippet = $this->snippetFromMethod(Annotation::class, 'cropHints');
+ $snippet->addLocal('annotation', new Annotation([
+ 'cropHintsAnnotation' => [
+ 'cropHints' => [[]]
+ ]
+ ]));
+
+ $res = $snippet->invoke('hints');
+ $this->assertInstanceOf(CropHint::class, $res->returnVal()[0]);
+ }
+
+ public function testWeb()
+ {
+ $snippet = $this->snippetFromMethod(Annotation::class, 'web');
+ $snippet->addLocal('annotation', new Annotation([
+ 'webDetection' => []
+ ]));
+
+ $res = $snippet->invoke('web');
+ $this->assertInstanceOf(Web::class, $res->returnVal());
+ }
+
public function testError()
{
$snippet = $this->snippetFromMethod(Annotation::class, 'error');
diff --git a/tests/snippets/Vision/ImageTest.php b/tests/snippets/Vision/ImageTest.php
index 3d2dac623442..5473e64df24a 100644
--- a/tests/snippets/Vision/ImageTest.php
+++ b/tests/snippets/Vision/ImageTest.php
@@ -29,7 +29,10 @@ class ImageTest extends SnippetTestCase
public function testImageFromServiceBuilder()
{
$snippet = $this->snippetFromClass(Image::class, 'default');
- $snippet->setLine(6, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$res = $snippet->invoke('image');
$this->assertInstanceOf(Image::class, $res->returnVal());
@@ -38,7 +41,10 @@ public function testImageFromServiceBuilder()
public function testDirectInstantiation()
{
$snippet = $this->snippetFromClass(Image::class, 'direct');
- $snippet->setLine(4, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$res = $snippet->invoke('image');
$this->assertInstanceOf(Image::class, $res->returnVal());
@@ -76,7 +82,10 @@ public function testMaxResults()
public function testFeatureShortcuts()
{
$snippet = $this->snippetFromClass(Image::class, 'shortcut');
- $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$res = $snippet->invoke('image');
$this->assertInstanceOf(Image::class, $res->returnVal());
@@ -85,7 +94,10 @@ public function testFeatureShortcuts()
public function testRequestObject()
{
$snippet = $this->snippetFromMethod(Image::class, 'requestObject');
- $snippet->setLine(2, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$res = $snippet->invoke('requestObj');
$this->assertTrue(array_key_exists('image', $res->returnVal()));
diff --git a/tests/snippets/Vision/VisionClientTest.php b/tests/snippets/Vision/VisionClientTest.php
index 9d543b2fc957..e24f00ee0b9f 100644
--- a/tests/snippets/Vision/VisionClientTest.php
+++ b/tests/snippets/Vision/VisionClientTest.php
@@ -35,8 +35,8 @@ class VisionClientTest extends SnippetTestCase
public function setUp()
{
$this->connection = $this->prophesize(ConnectionInterface::class);
- $this->client = new \VisionClientStub;
- $this->client->setConnection($this->connection->reveal());
+ $this->client = \Google\Cloud\Dev\stub(VisionClient::class);
+ $this->client->___setProperty('connection', $this->connection->reveal());
}
public function testClassWithServiceBuilder()
@@ -47,20 +47,15 @@ public function testClassWithServiceBuilder()
$this->assertInstanceOf(VisionClient::class, $res->returnVal());
}
- public function testClassDirectInstantiation()
- {
- $snippet = $this->snippetFromClass(VisionClient::class, 1);
- $res = $snippet->invoke('vision');
-
- $this->assertInstanceOf(VisionClient::class, $res->returnVal());
- }
-
public function testImage()
{
$snippet = $this->snippetFromMethod(VisionClient::class, 'image');
$snippet->addLocal('vision', $this->client);
- $snippet->setLine(0, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$res = $snippet->invoke('image');
@@ -72,7 +67,10 @@ public function testImageWithMaxResults()
$snippet = $this->snippetFromMethod(VisionClient::class, 'image', 1);
$snippet->addLocal('vision', $this->client);
- $snippet->setLine(2, '$imageResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$res = $snippet->invoke('image');
@@ -84,8 +82,15 @@ public function testImages()
$snippet = $this->snippetFromMethod(VisionClient::class, 'images');
$snippet->addLocal('vision', $this->client);
- $snippet->setLine(3, '$familyPhotoResource = fopen(\'php://temp\', \'r\');');
- $snippet->setLine(4, '$weddingPhotoResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
+
+ $snippet->replace(
+ "__DIR__ . '/assets/wedding-photo.jpg'",
+ "'php://temp'"
+ );
$res = $snippet->invoke('images');
$this->assertInstanceOf(Image::class, $res->returnVal()[0]);
@@ -97,7 +102,10 @@ public function testAnnotate()
$snippet = $this->snippetFromMethod(VisionClient::class, 'annotate');
$snippet->addLocal('vision', $this->client);
- $snippet->setLine(0, '$familyPhotoResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
$this->connection->annotate(Argument::any())
->shouldBeCalled()
@@ -107,7 +115,7 @@ public function testAnnotate()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('result');
@@ -119,8 +127,15 @@ public function testAnnotateBatch()
$snippet = $this->snippetFromMethod(VisionClient::class, 'annotateBatch');
$snippet->addLocal('vision', $this->client);
- $snippet->setLine(2, '$familyPhotoResource = fopen(\'php://temp\', \'r\');');
- $snippet->setLine(3, '$eiffelTowerResource = fopen(\'php://temp\', \'r\');');
+ $snippet->replace(
+ "__DIR__ . '/assets/family-photo.jpg'",
+ "'php://temp'"
+ );
+
+ $snippet->replace(
+ "__DIR__ . '/assets/eiffel-tower.jpg'",
+ "'php://temp'"
+ );
$this->connection->annotate(Argument::any())
->shouldBeCalled()
@@ -130,7 +145,7 @@ public function testAnnotateBatch()
]
]);
- $this->client->setConnection($this->connection->reveal());
+ $this->client->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('result');
diff --git a/tests/snippets/bootstrap.php b/tests/snippets/bootstrap.php
index af9eec686e44..298292891bbb 100644
--- a/tests/snippets/bootstrap.php
+++ b/tests/snippets/bootstrap.php
@@ -2,7 +2,7 @@
// Provide a project ID. If you're mocking your service calls (and if you aren't
// start now) you don't need anything else.
-putenv('GOOGLE_APPLICATION_CREDENTIALS='. __DIR__ .'/keyfile-stub.json');
+putenv('GOOGLE_APPLICATION_CREDENTIALS='. __DIR__ . '/keyfile-stub.json');
use Google\Cloud\Dev\Snippet\Container;
use Google\Cloud\Dev\Snippet\Coverage\Coverage;
@@ -12,7 +12,7 @@
require __DIR__ . '/../../vendor/autoload.php';
$parser = new Parser;
-$scanner = new Scanner($parser, __DIR__ .'/../../src');
+$scanner = new Scanner($parser, __DIR__ . '/../../src');
$coverage = new Coverage($scanner);
$coverage->buildListToCover();
@@ -22,42 +22,14 @@
register_shutdown_function(function () {
$uncovered = Container::$coverage->uncovered();
- if (!file_exists(__DIR__ .'/../../build')) {
- mkdir(__DIR__ .'/../../build', 0777, true);
+ if (!file_exists(__DIR__ . '/../../build')) {
+ mkdir(__DIR__ . '/../../build', 0777, true);
}
- file_put_contents(__DIR__ .'/../../build/snippets-uncovered.json', json_encode($uncovered, JSON_PRETTY_PRINT));
+ file_put_contents(__DIR__ . '/../../build/snippets-uncovered.json', json_encode($uncovered, JSON_PRETTY_PRINT));
if (!empty($uncovered)) {
- echo sprintf("\033[31mNOTICE: %s uncovered snippets! See build/snippets-uncovered.json for a report.\n", count($uncovered));
+ echo sprintf("\033[31mNOTICE: %s uncovered snippets! \033[0m See build/snippets-uncovered.json for a report.\n", count($uncovered));
+ exit(1);
}
});
-
-function stub($name, $extends)
-{
- $tpl = 'class %s extends %s {use \Google\Cloud\Dev\SetStubConnectionTrait; }';
-
- eval(sprintf($tpl, $name, $extends));
-}
-
-stub('AclStub', Google\Cloud\Storage\Acl::class);
-stub('BigQueryClientStub', Google\Cloud\BigQuery\BigQueryClient::class);
-stub('BucketStub', Google\Cloud\Storage\Bucket::class);
-stub('DatastoreClientStub', Google\Cloud\Datastore\DatastoreClient::class);
-stub('IamStub', Google\Cloud\Iam\Iam::class);
-stub('LoggerStub', Google\Cloud\Logging\Logger::class);
-stub('LoggingClientStub', Google\Cloud\Logging\LoggingClient::class);
-stub('MetricStub', Google\Cloud\Logging\Metric::class);
-stub('OperationStub', Google\Cloud\Datastore\Operation::class);
-stub('PubSubClientStub', Google\Cloud\PubSub\PubSubClient::class);
-stub('QueryResultsStub', Google\Cloud\BigQuery\QueryResults::class);
-stub('SinkStub', Google\Cloud\Logging\Sink::class);
-stub('SpeechClientStub', Google\Cloud\Speech\SpeechClient::class);
-stub('SpeechOperationStub', Google\Cloud\Speech\Operation::class);
-stub('StorageClientStub', Google\Cloud\Storage\StorageClient::class);
-stub('StorageObjectStub', Google\Cloud\Storage\StorageObject::class);
-stub('SubscriptionStub', Google\Cloud\PubSub\Subscription::class);
-stub('TableStub', Google\Cloud\BigQuery\Table::class);
-stub('TopicStub', Google\Cloud\PubSub\Topic::class);
-stub('TranslateClientStub', Google\Cloud\Translate\TranslateClient::class);
-stub('VisionClientStub', Google\Cloud\Vision\VisionClient::class);
diff --git a/tests/system/BigQuery/BigQueryTestCase.php b/tests/system/BigQuery/BigQueryTestCase.php
index d8f04a8c4a61..25f4bea43278 100644
--- a/tests/system/BigQuery/BigQueryTestCase.php
+++ b/tests/system/BigQuery/BigQueryTestCase.php
@@ -18,7 +18,7 @@
namespace Google\Cloud\Tests\System\BigQuery;
use Google\Cloud\BigQuery\BigQueryClient;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
use Google\Cloud\Storage\StorageClient;
class BigQueryTestCase extends \PHPUnit_Framework_TestCase
@@ -39,18 +39,7 @@ public static function setUpBeforeClass()
}
$keyFilePath = getenv('GOOGLE_CLOUD_PHP_TESTS_KEY_PATH');
- $schema = [
- 'fields' => [
- [
- 'name' => 'city',
- 'type' => 'STRING'
- ],
- [
- 'name' => 'state',
- 'type' => 'STRING'
- ]
- ]
- ];
+ $schema = json_decode(file_get_contents(__DIR__ . '/../data/table-schema.json'), true);
self::$bucket = (new StorageClient([
'keyFilePath' => $keyFilePath
]))->createBucket(uniqid(self::TESTING_PREFIX));
@@ -59,7 +48,9 @@ public static function setUpBeforeClass()
]);
self::$dataset = self::$client->createDataset(uniqid(self::TESTING_PREFIX));
self::$table = self::$dataset->createTable(uniqid(self::TESTING_PREFIX), [
- 'schema' => $schema
+ 'schema' => [
+ 'fields' => $schema
+ ]
]);
self::$hasSetUp = true;
}
diff --git a/tests/system/BigQuery/LoadDataAndQueryTest.php b/tests/system/BigQuery/LoadDataAndQueryTest.php
index 780b3569bb4b..5ef687afe88d 100644
--- a/tests/system/BigQuery/LoadDataAndQueryTest.php
+++ b/tests/system/BigQuery/LoadDataAndQueryTest.php
@@ -17,8 +17,12 @@
namespace Google\Cloud\Tests\System\BigQuery;
+use Google\Cloud\BigQuery\Bytes;
use Google\Cloud\BigQuery\Date;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
+use Google\Cloud\BigQuery\Time;
+use Google\Cloud\BigQuery\Timestamp;
+use Google\Cloud\BigQuery\ValueMapper;
use GuzzleHttp\Psr7;
/**
@@ -27,131 +31,221 @@
class LoadDataAndQueryTest extends BigQueryTestCase
{
private static $expectedRows = 0;
+ private $row;
+
+ public function setUp()
+ {
+ $this->row = [
+ 'Name' => 'Dave',
+ 'Age' => 101,
+ 'Weight' => 100.5,
+ 'IsMagic' => true,
+ 'Spells' => [
+ [
+ 'Name' => 'Summon Dragon',
+ 'LastUsed' => self::$client->timestamp(new \DateTime('2000-01-01 23:59:56 UTC')),
+ 'DiscoveredBy' => 'Bobby',
+ 'Properties' => [
+ [
+ 'Name' => 'Fire',
+ 'Power' => 300.2
+ ]
+ ],
+ 'Icon' => self::$client->bytes('icon')
+ ]
+ ],
+ 'ImportantDates' => [
+ 'TeaTime' => self::$client->time(new \DateTime('15:15:12')),
+ 'NextVacation' => self::$client->date(new \DateTime('2020-10-11')),
+ 'FavoriteTime' => new \DateTime('1920-01-01 15:15:12')
+ ],
+ 'FavoriteNumbers' => [10, 11]
+ ];
+ }
+
+ public function testInsertRowToTable()
+ {
+ self::$expectedRows++;
+ $insertResponse = self::$table->insertRow($this->row);
+ sleep(1);
+ $rows = iterator_to_array(self::$table->rows());
+ $actualRow = $rows[0];
+
+ $this->assertTrue($insertResponse->isSuccessful());
+ $this->assertEquals(self::$expectedRows, count($rows));
+
+ $expectedRow = $this->row;
+ $expectedBytes = $expectedRow['Spells'][0]['Icon'];
+ $actualBytes = $actualRow['Spells'][0]['Icon'];
+ unset($expectedRow['Spells'][0]['Icon']);
+ unset($actualRow['Spells'][0]['Icon']);
+
+ $this->assertEquals($expectedRow, $actualRow);
+ $this->assertEquals((string) $expectedBytes, (string) $actualBytes);
+ }
/**
- * @dataProvider rowProvider
+ * @depends testInsertRowToTable
+ * @dataProvider useLegacySqlProvider
*/
- public function testLoadsDataToTable($data)
+ public function testRunQuery($useLegacySql)
{
- $job = self::$table->load($data, [
- 'jobConfig' => [
- 'sourceFormat' => 'CSV'
- ]
+ $query = sprintf(
+ $useLegacySql
+ ? 'SELECT Name, Age, Weight, IsMagic, Spells.* FROM [%s.%s]'
+ : 'SELECT Name, Age, Weight, IsMagic, Spells FROM `%s.%s`',
+ self::$dataset->id(),
+ self::$table->id()
+ );
+ $results = self::$client->runQuery($query, [
+ 'useLegacySql' => $useLegacySql
]);
$backoff = new ExponentialBackoff(8);
- $backoff->execute(function () use ($job) {
- $job->reload();
+ $backoff->execute(function () use ($results) {
+ $results->reload();
- if (!$job->isComplete()) {
+ if (!$results->isComplete()) {
throw new \Exception();
}
});
- if (!$job->isComplete()) {
- $this->fail('Job failed to complete within the allotted time.');
+ if (!$results->isComplete()) {
+ $this->fail('Query did not complete within the allotted time.');
}
- self::$expectedRows += count(file(__DIR__ . '/../data/table-data.csv'));
- $actualRows = count(iterator_to_array(self::$table->rows()));
-
- $this->assertEquals(self::$expectedRows, $actualRows);
- }
-
- public function rowProvider()
- {
- $data = file_get_contents(__DIR__ . '/../data/table-data.csv');
-
- return [
- [$data],
- [fopen(__DIR__ . '/../data/table-data.csv', 'r')],
- [Psr7\stream_for($data)]
- ];
+ $actualRow = iterator_to_array($results->rows())[0];
+
+ if ($useLegacySql) {
+ $spells = $this->row['Spells'][0];
+
+ $this->assertEquals($this->row['Name'], $actualRow['Name']);
+ $this->assertEquals($this->row['Age'], $actualRow['Age']);
+ $this->assertEquals($this->row['Weight'], $actualRow['Weight']);
+ $this->assertEquals($this->row['IsMagic'], $actualRow['IsMagic']);
+ $this->assertEquals($spells['Name'], $actualRow['Spells_Name']);
+ $this->assertEquals($spells['LastUsed'], $actualRow['Spells_LastUsed']);
+ $this->assertEquals($spells['DiscoveredBy'], $actualRow['Spells_DiscoveredBy']);
+ $this->assertEquals($spells['Properties'][0]['Name'], $actualRow['Spells_Properties_Name']);
+ $this->assertEquals($spells['Properties'][0]['Power'], $actualRow['Spells_Properties_Power']);
+ $this->assertEquals((string) $spells['Icon'], (string) $actualRow['Spells_Icon']);
+ } else {
+ $expectedRow = $this->row;
+ $expectedBytes = $expectedRow['Spells'][0]['Icon'];
+ $actualBytes = $actualRow['Spells'][0]['Icon'];
+ unset($expectedRow['ImportantDates']);
+ unset($expectedRow['FavoriteNumbers']);
+ unset($expectedRow['Spells'][0]['Icon']);
+ unset($actualRow['Spells'][0]['Icon']);
+
+ $this->assertEquals($expectedRow, $actualRow);
+ $this->assertEquals((string) $expectedBytes, (string) $actualBytes);
+ }
}
/**
- * @depends testLoadsDataToTable
+ * @depends testInsertRowToTable
+ * @dataProvider useLegacySqlProvider
*/
- public function testLoadsDataFromStorageToTable()
+ public function testRunQueryAsJob($useLegacySql)
{
- $object = self::$bucket->upload(
- fopen(__DIR__ . '/../data/table-data.csv', 'r')
+ $query = sprintf(
+ $useLegacySql
+ ? 'SELECT FavoriteNumbers, ImportantDates.* FROM [%s.%s]'
+ : 'SELECT FavoriteNumbers, ImportantDates FROM `%s.%s`',
+ self::$dataset->id(),
+ self::$table->id()
);
- self::$deletionQueue[] = $object;
-
- $job = self::$table->loadFromStorage($object, [
+ $job = self::$client->runQueryAsJob($query, [
'jobConfig' => [
- 'sourceFormat' => 'CSV'
+ 'useLegacySql' => $useLegacySql
]
]);
+ $results = $job->queryResults();
$backoff = new ExponentialBackoff(8);
- $backoff->execute(function () use ($job) {
- $job->reload();
+ $backoff->execute(function () use ($results) {
+ $results->reload();
- if (!$job->isComplete()) {
+ if (!$results->isComplete()) {
throw new \Exception();
}
});
- if (!$job->isComplete()) {
- $this->fail('Job failed to complete within the allotted time.');
+
+ if (!$results->isComplete()) {
+ $this->fail('Query did not complete within the allotted time.');
}
- self::$expectedRows += count(file(__DIR__ . '/../data/table-data.csv'));
- $actualRows = count(iterator_to_array(self::$table->rows()));
+ $actualRows = iterator_to_array($results->rows());
- $this->assertEquals(self::$expectedRows, $actualRows);
+ if ($useLegacySql) {
+ $dates = $this->row['ImportantDates'];
+ $numbers = $this->row['FavoriteNumbers'];
+
+ $this->assertEquals($numbers[0], $actualRows[0]['FavoriteNumbers']);
+ $this->assertEquals($numbers[1], $actualRows[1]['FavoriteNumbers']);
+ $this->assertEquals($dates['TeaTime'], $actualRows[0]['ImportantDates_TeaTime']);
+ $this->assertEquals($dates['NextVacation'], $actualRows[0]['ImportantDates_NextVacation']);
+ $this->assertEquals($dates['FavoriteTime'], $actualRows[0]['ImportantDates_FavoriteTime']);
+ } else {
+ $expectedRow = [
+ 'FavoriteNumbers' => $this->row['FavoriteNumbers'],
+ 'ImportantDates' => $this->row['ImportantDates']
+ ];
+
+ $this->assertEquals($expectedRow, $actualRows[0]);
+ }
}
- /**
- * @depends testLoadsDataFromStorageToTable
- */
- public function testInsertRowToTable()
+ public function useLegacySqlProvider()
{
- self::$expectedRows++;
- $insertResponse = self::$table->insertRow([
- 'city' => 'Detroit',
- 'state' => 'MI'
- ]);
-
- $this->assertTrue($insertResponse->isSuccessful());
+ return [
+ [true],
+ [false]
+ ];
}
- /**
- * @depends testInsertRowToTable
- */
- public function testInsertRowsToTable()
+ public function testRunQueryWithNamedParameters()
{
- $rows = [
- [
- 'data' => [
- 'city' => 'Detroit',
- 'state' => 'MI'
+ $query = 'SELECT'
+ . '@structType as structType,'
+ . '@arrayStruct as arrayStruct,'
+ . '@nestedStruct as nestedStruct,'
+ . '@arrayType as arrayType,'
+ . '@name as name,'
+ . '@int as int,'
+ . '@float as float,'
+ . '@timestamp as timestamp,'
+ . '@datetime as datetime,'
+ . '@date as date,'
+ . '@time as time,'
+ . '@bytes as bytes';
+
+ $bytes = self::$client->bytes('123');
+ $params = [
+ 'structType' => [
+ 'hello' => 'world'
+ ],
+ 'arrayStruct' => [
+ [
+ 'hello' => 'world'
]
],
- [
- 'data' => [
- 'city' => 'Ann Arbor',
- 'state' => 'MI'
+ 'nestedStruct' => [
+ 'hello' => [
+ 'wor' => 'ld'
]
- ]
+ ],
+ 'arrayType' => [1,2,3],
+ 'name' => 'Dave',
+ 'int' => 5,
+ 'float' => 5.5,
+ 'timestamp' => self::$client->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')),
+ 'datetime' => new \DateTime('2003-02-05 11:15:02.421827Z'),
+ 'date' => self::$client->date(new \DateTime('2003-12-12')),
+ 'time' => self::$client->time(new \DateTime('11:15:02')),
+ 'bytes' => $bytes
];
- self::$expectedRows += count($rows);
- $insertResponse = self::$table->insertRows($rows);
+ $results = self::$client->runQuery($query, ['parameters' => $params]);
- $this->assertTrue($insertResponse->isSuccessful());
- }
-
- /**
- * @depends testInsertRowsToTable
- */
- public function testRunQuery()
- {
- $results = self::$client->runQuery(
- sprintf(
- 'SELECT * FROM [%s.%s]',
- self::$dataset->id(),
- self::$table->id()
- )
- );
$backoff = new ExponentialBackoff(8);
$backoff->execute(function () use ($results) {
$results->reload();
@@ -165,30 +259,20 @@ public function testRunQuery()
$this->fail('Query did not complete within the allotted time.');
}
- $actualRows = count(iterator_to_array($results->rows()));
+ $actualRow = iterator_to_array($results->rows())[0];
+ $actualBytes = $actualRow['bytes'];
+ unset($params['bytes']);
+ unset($actualRow['bytes']);
- $this->assertEquals(self::$expectedRows, $actualRows);
+ $this->assertEquals($params, $actualRow);
+ $this->assertEquals((string) $bytes, (string) $actualBytes);
}
- public function testRunQueryWithNamedParameters()
+ public function testRunQueryWithPositionalParameters()
{
- $date = '2000-01-01';
- $query = 'WITH data AS'
- . '(SELECT "Dave" as name, DATE("1999-01-01") as date, 1.1 as floatNum, 1 as intNum, false as boolVal '
- . 'UNION ALL '
- . 'SELECT "John" as name, DATE("2000-01-01") as date, 1.2 as floatNum, 2 as intNum, true as boolVal) '
- . 'SELECT * FROM data '
- . 'WHERE name = @name AND date >= @date AND floatNum = @numbers.floatNum AND intNum = @numbers.intNum AND boolVal = @boolVal';
-
- $results = self::$client->runQuery($query, [
+ $results = self::$client->runQuery('SELECT 1 IN UNNEST(?) AS arr', [
'parameters' => [
- 'name' => 'John',
- 'date' => self::$client->date(new \DateTime($date)),
- 'numbers' => [
- 'floatNum' => 1.2,
- 'intNum' => 2,
- ],
- 'boolVal' => true
+ [1, 2, 3]
]
]);
$backoff = new ExponentialBackoff(8);
@@ -206,25 +290,21 @@ public function testRunQueryWithNamedParameters()
$actualRows = iterator_to_array($results->rows());
$expectedRows = [
- [
- 'name' => 'John',
- 'floatNum' => 1.2,
- 'intNum' => 2,
- 'boolVal' => true,
- 'date' => new Date(new \DateTime($date))
- ]
+ ['arr' => true]
];
$this->assertEquals($expectedRows, $actualRows);
}
- public function testRunQueryWithPositionalParameters()
+ public function testRunQueryAsJobWithNamedParameters()
{
- $results = self::$client->runQuery('SELECT 1 IN UNNEST(?) AS arr', [
+ $query = 'SELECT @int as int';
+ $job = self::$client->runQueryAsJob($query, [
'parameters' => [
- [1, 2, 3]
+ 'int' => 5
]
]);
+ $results = $job->queryResults();
$backoff = new ExponentialBackoff(8);
$backoff->execute(function () use ($results) {
$results->reload();
@@ -239,25 +319,18 @@ public function testRunQueryWithPositionalParameters()
}
$actualRows = iterator_to_array($results->rows());
- $expectedRows = [
- ['arr' => true]
- ];
+ $expectedRows = [['int' => 5]];
$this->assertEquals($expectedRows, $actualRows);
}
- /**
- * @depends testInsertRowsToTable
- */
- public function testRunQueryAsJob()
+ public function testRunQueryAsJobWithPositionalParameters()
{
- $job = self::$client->runQueryAsJob(
- sprintf(
- 'SELECT * FROM [%s.%s]',
- self::$dataset->id(),
- self::$table->id()
- )
- );
+ $job = self::$client->runQueryAsJob('SELECT 1 IN UNNEST(?) AS arr', [
+ 'parameters' => [
+ [1, 2, 3]
+ ]
+ ]);
$results = $job->queryResults();
$backoff = new ExponentialBackoff(8);
$backoff->execute(function () use ($results) {
@@ -272,86 +345,102 @@ public function testRunQueryAsJob()
$this->fail('Query did not complete within the allotted time.');
}
- $actualRows = count(iterator_to_array($results->rows()));
+ $actualRows = iterator_to_array($results->rows());
+ $expectedRows = [
+ ['arr' => true]
+ ];
- $this->assertEquals(self::$expectedRows, $actualRows);
+ $this->assertEquals($expectedRows, $actualRows);
}
- public function testRunQueryAsJobWithNamedParameters()
+ /**
+ * @dataProvider rowProvider
+ * @depends testInsertRowToTable
+ */
+ public function testLoadsDataToTable($data)
{
- $date = '2000-01-01';
- $query = 'WITH data AS'
- . '(SELECT "Dave" as name, DATE("1999-01-01") as date, 1.1 as floatNum, 1 as intNum, true as boolVal '
- . 'UNION ALL '
- . 'SELECT "John" as name, DATE("2000-01-01") as date, 1.2 as floatNum, 2 as intNum, false as boolVal) '
- . 'SELECT * FROM data '
- . 'WHERE name = @name AND date >= @date AND floatNum = @numbers.floatNum AND intNum = @numbers.intNum AND boolVal = @boolVal';
-
- $job = self::$client->runQueryAsJob($query, [
- 'parameters' => [
- 'name' => 'John',
- 'date' => self::$client->date(new \DateTime($date)),
- 'numbers' => [
- 'floatNum' => 1.2,
- 'intNum' => 2,
- ],
- 'boolVal' => false
+ $job = self::$table->load($data, [
+ 'jobConfig' => [
+ 'sourceFormat' => 'NEWLINE_DELIMITED_JSON'
]
]);
- $results = $job->queryResults();
$backoff = new ExponentialBackoff(8);
- $backoff->execute(function () use ($results) {
- $results->reload();
+ $backoff->execute(function () use ($job) {
+ $job->reload();
- if (!$results->isComplete()) {
+ if (!$job->isComplete()) {
throw new \Exception();
}
});
- if (!$results->isComplete()) {
- $this->fail('Query did not complete within the allotted time.');
+ if (!$job->isComplete()) {
+ $this->fail('Job failed to complete within the allotted time.');
}
- $actualRows = iterator_to_array($results->rows());
- $expectedRows = [
- [
- 'name' => 'John',
- 'floatNum' => 1.2,
- 'intNum' => 2,
- 'boolVal' => false,
- 'date' => new Date(new \DateTime($date))
- ]
- ];
+ self::$expectedRows += count(file(__DIR__ . '/../data/table-data.json'));
+ $actualRows = count(iterator_to_array(self::$table->rows()));
- $this->assertEquals($expectedRows, $actualRows);
+ $this->assertEquals(self::$expectedRows, $actualRows);
}
- public function testRunQueryAsJobWithPositionalParameters()
+ public function rowProvider()
{
- $job = self::$client->runQueryAsJob('SELECT 1 IN UNNEST(?) AS arr', [
- 'parameters' => [
- [1, 2, 3]
+ $data = file_get_contents(__DIR__ . '/../data/table-data.json');
+
+ return [
+ [$data],
+ [fopen(__DIR__ . '/../data/table-data.json', 'r')],
+ [Psr7\stream_for($data)]
+ ];
+ }
+
+ /**
+ * @depends testLoadsDataToTable
+ */
+ public function testLoadsDataFromStorageToTable()
+ {
+ $object = self::$bucket->upload(
+ fopen(__DIR__ . '/../data/table-data.json', 'r')
+ );
+ self::$deletionQueue[] = $object;
+
+ $job = self::$table->loadFromStorage($object, [
+ 'jobConfig' => [
+ 'sourceFormat' => 'NEWLINE_DELIMITED_JSON'
]
]);
- $results = $job->queryResults();
$backoff = new ExponentialBackoff(8);
- $backoff->execute(function () use ($results) {
- $results->reload();
+ $backoff->execute(function () use ($job) {
+ $job->reload();
- if (!$results->isComplete()) {
+ if (!$job->isComplete()) {
throw new \Exception();
}
});
-
- if (!$results->isComplete()) {
- $this->fail('Query did not complete within the allotted time.');
+ if (!$job->isComplete()) {
+ $this->fail('Job failed to complete within the allotted time.');
}
- $actualRows = iterator_to_array($results->rows());
- $expectedRows = [
- ['arr' => true]
+ self::$expectedRows += count(file(__DIR__ . '/../data/table-data.json'));
+ $actualRows = count(iterator_to_array(self::$table->rows()));
+
+ $this->assertEquals(self::$expectedRows, $actualRows);
+ }
+
+ /**
+ * @depends testLoadsDataToTable
+ */
+ public function testInsertRowsToTable()
+ {
+ $rows = [
+ ['data' => $this->row],
+ ['data' => $this->row]
];
+ self::$expectedRows += count($rows);
+ $insertResponse = self::$table->insertRows($rows);
+ $actualRows = count(iterator_to_array(self::$table->rows()));
- $this->assertEquals($expectedRows, $actualRows);
+ $this->assertTrue($insertResponse->isSuccessful());
+ $this->assertEquals(self::$expectedRows, $actualRows);
}
}
diff --git a/tests/system/BigQuery/ManageTablesTest.php b/tests/system/BigQuery/ManageTablesTest.php
index a2f3267fe052..bba2824e2bc2 100644
--- a/tests/system/BigQuery/ManageTablesTest.php
+++ b/tests/system/BigQuery/ManageTablesTest.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Tests\System\BigQuery;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
/**
* @group bigquery
@@ -97,7 +97,11 @@ public function testExportsTable()
uniqid(self::TESTING_PREFIX)
);
self::$deletionQueue[] = $object;
- $job = self::$table->export($object);
+ $job = self::$table->export($object, [
+ 'jobConfig' => [
+ 'destinationFormat' => 'NEWLINE_DELIMITED_JSON'
+ ]
+ ]);
$backoff = new ExponentialBackoff(8);
$backoff->execute(function () use ($job) {
@@ -111,6 +115,7 @@ public function testExportsTable()
if (!$job->isComplete()) {
$this->fail('Job failed to complete within the allotted time.');
}
+
$this->assertArrayNotHasKey('errorResult', $job->info()['status']);
}
diff --git a/tests/system/Datastore/DatastoreTestCase.php b/tests/system/Datastore/DatastoreTestCase.php
index aa726c76b8f3..4363eee9c863 100644
--- a/tests/system/Datastore/DatastoreTestCase.php
+++ b/tests/system/Datastore/DatastoreTestCase.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Tests\System\Datastore;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
use Google\Cloud\Datastore\DatastoreClient;
class DatastoreTestCase extends \PHPUnit_Framework_TestCase
diff --git a/tests/system/Datastore/RunQueryTest.php b/tests/system/Datastore/RunQueryTest.php
index 0677fe9e1339..32c34693a4ec 100644
--- a/tests/system/Datastore/RunQueryTest.php
+++ b/tests/system/Datastore/RunQueryTest.php
@@ -63,6 +63,12 @@ public static function setUpBeforeClass()
self::$client->entity($key2, self::$data[2]),
self::$client->entity($key3, self::$data[3])
]);
+
+ // on rare occasions the queries below are returning no results when
+ // triggered immediately after an insert operation. the sleep here
+ // is intended to help alleviate this issue.
+ sleep(1);
+
self::$deletionQueue[] = self::$ancestor;
self::$deletionQueue[] = $key1;
self::$deletionQueue[] = $key2;
diff --git a/tests/system/Datastore/SaveAndModifyTest.php b/tests/system/Datastore/SaveAndModifyTest.php
index fa366daf8ef6..7ec8101224f4 100644
--- a/tests/system/Datastore/SaveAndModifyTest.php
+++ b/tests/system/Datastore/SaveAndModifyTest.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Tests\System\Datastore;
-use Google\Cloud\Int64;
+use Google\Cloud\Core\Int64;
/**
* @group datastore
diff --git a/tests/system/Logging/LoggingTestCase.php b/tests/system/Logging/LoggingTestCase.php
index 9c93fec7d2cc..fdecd9cae58f 100644
--- a/tests/system/Logging/LoggingTestCase.php
+++ b/tests/system/Logging/LoggingTestCase.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Tests\System\Logging;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\Logging\LoggingClient;
use Google\Cloud\PubSub\PubSubClient;
diff --git a/tests/system/Logging/WriteAndListEntryTest.php b/tests/system/Logging/WriteAndListEntryTest.php
index 12d74a847f1f..44b5a9e9e6ba 100644
--- a/tests/system/Logging/WriteAndListEntryTest.php
+++ b/tests/system/Logging/WriteAndListEntryTest.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Tests\System\Logging;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
/**
* @group logging
diff --git a/tests/system/NaturalLanguage/NaturalLanguageTestCase.php b/tests/system/NaturalLanguage/NaturalLanguageTestCase.php
index cc6da38eb219..956ab308c4d9 100644
--- a/tests/system/NaturalLanguage/NaturalLanguageTestCase.php
+++ b/tests/system/NaturalLanguage/NaturalLanguageTestCase.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Tests\System\NaturalLanguage;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\NaturalLanguage\NaturalLanguageClient;
use Google\Cloud\PubSub\PubSubClient;
diff --git a/tests/system/PubSub/PubSubTestCase.php b/tests/system/PubSub/PubSubTestCase.php
index 07dd04d87ad0..4e33b77d0268 100644
--- a/tests/system/PubSub/PubSubTestCase.php
+++ b/tests/system/PubSub/PubSubTestCase.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Tests\System\PubSub;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
use Google\Cloud\PubSub\PubSubClient;
class PubSubTestCase extends \PHPUnit_Framework_TestCase
diff --git a/tests/system/PubSub/PublishAndPullTest.php b/tests/system/PubSub/PublishAndPullTest.php
index e8347eb1af41..61e3df1bf720 100644
--- a/tests/system/PubSub/PublishAndPullTest.php
+++ b/tests/system/PubSub/PublishAndPullTest.php
@@ -42,7 +42,7 @@ public function testPublishMessageAndPull($client)
];
$topic->publish($message);
- $messages = iterator_to_array($sub->pull());
+ $messages = $sub->pull();
$sub->modifyAckDeadline($messages[0], 15);
$sub->acknowledge($messages[0]);
@@ -79,7 +79,7 @@ public function testPublishMessagesAndPull($client)
$topic->publishBatch($messages);
- $actualMessages = iterator_to_array($sub->pull());
+ $actualMessages = $sub->pull();
$sub->modifyAckDeadlineBatch($actualMessages, 15);
$sub->acknowledgeBatch($actualMessages);
diff --git a/tests/system/Storage/ManageAclTest.php b/tests/system/Storage/ManageAclTest.php
index 5cf495fff7ff..872d44735da1 100644
--- a/tests/system/Storage/ManageAclTest.php
+++ b/tests/system/Storage/ManageAclTest.php
@@ -18,7 +18,7 @@
namespace Google\Cloud\Tests\System\Storage;
use Google\Cloud\Storage\Acl;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
/**
* @group storage
diff --git a/tests/system/Storage/StorageTestCase.php b/tests/system/Storage/StorageTestCase.php
index 2cc5c72f15a5..461a54aa73ca 100644
--- a/tests/system/Storage/StorageTestCase.php
+++ b/tests/system/Storage/StorageTestCase.php
@@ -17,7 +17,7 @@
namespace Google\Cloud\Tests\System\Storage;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
use Google\Cloud\Storage\StorageClient;
class StorageTestCase extends \PHPUnit_Framework_TestCase
@@ -39,7 +39,8 @@ public static function setUpBeforeClass()
self::$client = new StorageClient([
'keyFilePath' => getenv('GOOGLE_CLOUD_PHP_TESTS_KEY_PATH')
]);
- self::$bucket = self::$client->createBucket(uniqid(self::TESTING_PREFIX));
+ $bucket = getenv('BUCKET') ?: uniqid(self::TESTING_PREFIX);
+ self::$bucket = self::$client->createBucket($bucket);
self::$object = self::$bucket->upload('somedata', ['name' => uniqid(self::TESTING_PREFIX)]);
self::$hasSetUp = true;
}
@@ -62,5 +63,3 @@ public static function tearDownFixtures()
}
}
}
-
-
diff --git a/tests/system/Storage/StreamWrapper/DirectoryTest.php b/tests/system/Storage/StreamWrapper/DirectoryTest.php
new file mode 100644
index 000000000000..a7e1883430af
--- /dev/null
+++ b/tests/system/Storage/StreamWrapper/DirectoryTest.php
@@ -0,0 +1,84 @@
+upload('somedata', ['name' => 'some_folder/1.txt']);
+ self::$bucket->upload('somedata', ['name' => 'some_folder/2.txt']);
+ self::$bucket->upload('somedata', ['name' => 'some_folder/3.txt']);
+ }
+
+ public function testMkDir()
+ {
+ $dir = self::generateUrl('test_directory');
+ $this->assertTrue(mkdir($dir));
+ $this->assertTrue(file_exists($dir . '/'));
+ $this->assertTrue(is_dir($dir . '/'));
+ }
+
+ public function testRmDir()
+ {
+ $dir = self::generateUrl('test_directory/');
+ $this->assertTrue(rmdir($dir));
+ $this->assertFalse(file_exists($dir . '/'));
+ }
+
+ public function testMkDirCreatesBucket()
+ {
+ $newBucket = uniqid(self::TESTING_PREFIX);
+ $bucketUrl = "gs://$newBucket/";
+ $this->assertTrue(mkdir($bucketUrl, 0700));
+
+ $bucket = self::$client->bucket($newBucket);
+ $this->assertTrue($bucket->exists());
+ $this->assertTrue(rmdir($bucketUrl));
+ }
+
+ public function testListDirectory()
+ {
+ $dir = self::generateUrl('some_folder');
+ $fd = opendir($dir);
+ $this->assertEquals('some_folder/1.txt', readdir($fd));
+ $this->assertEquals('some_folder/2.txt', readdir($fd));
+ rewinddir($fd);
+ $this->assertEquals('some_folder/1.txt', readdir($fd));
+ closedir($fd);
+ }
+
+ public function testScanDirectory()
+ {
+ $dir = self::generateUrl('some_folder');
+ $expected = [
+ 'some_folder/1.txt',
+ 'some_folder/2.txt',
+ 'some_folder/3.txt',
+ ];
+ $this->assertEquals($expected, scandir($dir));
+ $this->assertEquals(array_reverse($expected), scandir($dir, SCANDIR_SORT_DESCENDING));
+ }
+}
diff --git a/tests/system/Storage/StreamWrapper/ImageTest.php b/tests/system/Storage/StreamWrapper/ImageTest.php
new file mode 100644
index 000000000000..9ea0769db235
--- /dev/null
+++ b/tests/system/Storage/StreamWrapper/ImageTest.php
@@ -0,0 +1,78 @@
+upload(
+ $contents,
+ ['name' => 'exif.jpg']
+ );
+ $contents = file_get_contents(self::TEST_IMAGE);
+ self::$bucket->upload(
+ $contents,
+ ['name' => 'plain.jpg']
+ );
+ }
+
+ /**
+ * @dataProvider imageProvider
+ */
+ public function testGetImageSize($image, $width, $height)
+ {
+ $url = self::generateUrl($image);
+ $size = getimagesize($url);
+ $this->assertEquals($width, $size[0]);
+ $this->assertEquals($height, $size[1]);
+ }
+
+ /**
+ * @dataProvider imageProvider
+ */
+ public function testGetImageSizeWithInfo($image, $width, $height)
+ {
+ $url = self::generateUrl($image);
+ $info = array();
+ $size = getimagesize($url, $info);
+ $this->assertEquals($width, $size[0]);
+ $this->assertEquals($height, $size[1]);
+ $this->assertTrue(count(array_keys($info)) > 1);
+ }
+
+ public function imageProvider()
+ {
+ return [
+ ['plain.jpg', 1956, 960],
+ ['exif.jpg', 3960, 2640],
+ ];
+ }
+}
diff --git a/tests/system/Storage/StreamWrapper/ReadTest.php b/tests/system/Storage/StreamWrapper/ReadTest.php
new file mode 100644
index 000000000000..6f9dbd619aee
--- /dev/null
+++ b/tests/system/Storage/StreamWrapper/ReadTest.php
@@ -0,0 +1,63 @@
+file = self::generateUrl(self::$object->name());
+ }
+
+ public function testFread()
+ {
+ $fd = fopen($this->file, 'r');
+ $expected = 'somedata';
+ $this->assertEquals($expected, fread($fd, strlen($expected)));
+ $this->assertTrue(fclose($fd));
+ }
+
+ public function testFileGetContents()
+ {
+ $this->assertEquals('somedata', file_get_contents($this->file));
+ }
+
+ public function testGetLines()
+ {
+ $fd = fopen($this->file, 'r');
+ $expected = 'somedata';
+ $this->assertEquals($expected, fgets($fd));
+ $this->assertTrue(fclose($fd));
+ }
+
+ public function testEof()
+ {
+ $fd = fopen($this->file, 'r');
+ $this->assertFalse(feof($fd));
+ fread($fd, 1000);
+ $this->assertTrue(feof($fd));
+ $this->assertTrue(fclose($fd));
+ }
+
+}
diff --git a/tests/system/Storage/StreamWrapper/RenameTest.php b/tests/system/Storage/StreamWrapper/RenameTest.php
new file mode 100644
index 000000000000..9ea898393fd6
--- /dev/null
+++ b/tests/system/Storage/StreamWrapper/RenameTest.php
@@ -0,0 +1,54 @@
+upload('somedata', ['name' => self::TEST_FILE]);
+ }
+
+ public function testRenameFile()
+ {
+ $oldFile = self::generateUrl(self::TEST_FILE);
+ $newFile = self::generateUrl(self::NEW_TEST_FILE);
+ $this->assertTrue(rename($oldFile, $newFile));
+ $this->assertTrue(file_exists($newFile));
+ }
+
+ public function testRenameDirectory()
+ {
+ $oldFolder = self::generateUrl(dirname(self::TEST_FILE));
+ $newFolder = self::generateUrl('new_folder');
+ $newFile = $newFolder . '/bar.txt';
+ $this->assertTrue(rename($oldFolder, $newFolder));
+ $this->assertTrue(file_exists($newFile));
+ }
+
+}
diff --git a/tests/system/Storage/StreamWrapper/StreamWrapperTestCase.php b/tests/system/Storage/StreamWrapper/StreamWrapperTestCase.php
new file mode 100644
index 000000000000..8928ff7da42b
--- /dev/null
+++ b/tests/system/Storage/StreamWrapper/StreamWrapperTestCase.php
@@ -0,0 +1,46 @@
+registerStreamWrapper();
+ }
+
+ public static function tearDownAfterClass()
+ {
+ self::$client->unregisterStreamWrapper();
+ parent::tearDownAfterClass();
+ }
+
+ protected static function generateUrl($file)
+ {
+ $bucketName = self::$bucket->name();
+ return "gs://$bucketName/$file";
+ }
+
+}
diff --git a/tests/system/Storage/StreamWrapper/UrlStatTest.php b/tests/system/Storage/StreamWrapper/UrlStatTest.php
new file mode 100644
index 000000000000..aefa2d8bc6c0
--- /dev/null
+++ b/tests/system/Storage/StreamWrapper/UrlStatTest.php
@@ -0,0 +1,107 @@
+name());
+ self::$dirUrl = self::generateUrl('some_folder/');
+ mkdir(self::$dirUrl);
+ }
+
+ public function testUrlStatFile()
+ {
+ $stat = stat(self::$fileUrl);
+ $this->assertEquals(33206, $stat['mode']);
+ }
+
+ public function testUrlStatDirectory()
+ {
+ $stat = stat(self::$dirUrl);
+ $this->assertEquals(16895, $stat['mode']);
+ }
+
+ public function testStatOnOpenFileForWrite()
+ {
+ $fd = fopen(self::$fileUrl, 'w');
+ $stat = fstat($fd);
+ $this->assertEquals(33206, $stat['mode']);
+ }
+
+ public function testStatOnOpenFileForRead()
+ {
+ $fd = fopen(self::$fileUrl, 'r');
+ $stat = fstat($fd);
+ $this->assertEquals(33060, $stat['mode']);
+ }
+
+ public function testIsWritable()
+ {
+ $this->assertTrue(is_writable(self::$dirUrl));
+ $this->assertTrue(is_writable(self::$fileUrl));
+ }
+
+ public function testIsReadable()
+ {
+ $this->assertTrue(is_readable(self::$dirUrl));
+ $this->assertTrue(is_readable(self::$fileUrl));
+ }
+
+ public function testFileExists()
+ {
+ $this->assertTrue(file_exists(self::$dirUrl));
+ $this->assertTrue(file_exists(self::$fileUrl));
+ }
+
+ public function testIsLink()
+ {
+ $this->assertFalse(is_link(self::$dirUrl));
+ $this->assertFalse(is_link(self::$fileUrl));
+ }
+
+ public function testIsExecutable()
+ {
+ // php returns false for is_executable if the file is a directory
+ // https://github.com/php/php-src/blob/master/ext/standard/filestat.c#L907
+ $this->assertFalse(is_executable(self::$dirUrl));
+ $this->assertFalse(is_executable(self::$fileUrl));
+ }
+
+ public function testIsFile()
+ {
+ $this->assertTrue(is_file(self::$fileUrl));
+ $this->assertFalse(is_file(self::$dirUrl));
+ }
+
+ public function testIsDir()
+ {
+ $this->assertTrue(is_dir(self::$dirUrl));
+ $this->assertFalse(is_dir(self::$fileUrl));
+ }
+
+}
diff --git a/tests/system/Storage/StreamWrapper/WriteTest.php b/tests/system/Storage/StreamWrapper/WriteTest.php
new file mode 100644
index 000000000000..74d3bb45c6f9
--- /dev/null
+++ b/tests/system/Storage/StreamWrapper/WriteTest.php
@@ -0,0 +1,73 @@
+fileUrl = self::generateUrl('output.txt');
+ unlink($this->fileUrl);
+ }
+
+ public function tearDown()
+ {
+ unlink($this->fileUrl);
+ }
+
+ public function testFilePutContents()
+ {
+ $this->assertFalse(file_exists($this->fileUrl));
+
+ $output = 'This is a test';
+ $this->assertEquals(strlen($output), file_put_contents($this->fileUrl, $output));
+
+ $this->assertTrue(file_exists($this->fileUrl));
+ }
+
+ public function testFwrite()
+ {
+ $this->assertFalse(file_exists($this->fileUrl));
+
+ $output = 'This is a test';
+ $fd = fopen($this->fileUrl, 'w');
+ $this->assertEquals(strlen($output), fwrite($fd, $output));
+ $this->assertTrue(fclose($fd));
+
+ $this->assertTrue(file_exists($this->fileUrl));
+ }
+
+ public function testStreamingWrite()
+ {
+ $this->assertFalse(file_exists($this->fileUrl));
+
+ $fp = fopen($this->fileUrl, 'w');
+ for($i = 0; $i < 20000; $i++) {
+ fwrite($fp, "Line Number: $i\n");
+ }
+ fclose($fp);
+
+ $this->assertTrue(file_exists($this->fileUrl));
+ }
+}
diff --git a/tests/system/Vision/AnnotationsTest.php b/tests/system/Vision/AnnotationsTest.php
new file mode 100644
index 000000000000..cd160140ade6
--- /dev/null
+++ b/tests/system/Vision/AnnotationsTest.php
@@ -0,0 +1,169 @@
+client = parent::$vision;
+ }
+
+ public function testAnnotate()
+ {
+ $image = $this->client->image(
+ file_get_contents($this->getFixtureFilePath('landmark.jpg')) , [
+ 'LANDMARK_DETECTION',
+ 'SAFE_SEARCH_DETECTION',
+ 'IMAGE_PROPERTIES',
+ 'CROP_HINTS',
+ 'WEB_DETECTION'
+ ]
+ );
+
+ $res = $this->client->annotate($image);
+ $this->assertInstanceOf(Annotation::class, $res);
+
+ // Landmarks
+ $this->assertInstanceOf(Entity::class, $res->landmarks()[0]);
+ $this->assertEquals('Eiffel Tower', $res->landmarks()[0]->description());
+
+ // Safe Search
+ $this->assertInstanceOf(SafeSearch::class, $res->safeSearch());
+ $this->assertEquals('VERY_UNLIKELY', $res->safeSearch()->adult());
+ $this->assertEquals('VERY_UNLIKELY', $res->safeSearch()->spoof());
+ $this->assertEquals('VERY_UNLIKELY', $res->safeSearch()->medical());
+ $this->assertEquals('VERY_UNLIKELY', $res->safeSearch()->violence());
+ $this->assertFalse($res->safeSearch()->isAdult());
+ $this->assertFalse($res->safeSearch()->isSpoof());
+ $this->assertFalse($res->safeSearch()->isMedical());
+ $this->assertFalse($res->safeSearch()->isViolent());
+
+ // Image Properties
+ $this->assertInstanceOf(ImageProperties::class, $res->imageProperties());
+ $this->assertTrue(is_array($res->imageProperties()->colors()));
+
+ // Crop Hints
+ $this->assertInstanceOf(CropHint::class, $res->cropHints()[0]);
+ $this->assertTrue(isset($res->cropHints()[0]->boundingPoly()['vertices']));
+ $this->assertTrue(is_float($res->cropHints()[0]->confidence()));
+ $this->assertTrue(!is_null($res->cropHints()[0]->importanceFraction()));
+
+ // Web Annotation
+ $this->assertInstanceOf(Web::class, $res->web());
+ $this->assertInstanceOf(WebEntity::class, $res->web()->entities()[0]);
+
+ $desc = array_filter($res->web()->entities(), function ($e) {
+ return ($e->description() === 'Eiffel Tower');
+ });
+ $this->assertTrue(count($desc) > 0);
+
+ $this->assertInstanceOf(WebImage::class, $res->web()->matchingImages()[0]);
+ $this->assertInstanceOf(WebImage::class, $res->web()->partialMatchingImages()[0]);
+ $this->assertInstanceOf(WebPage::class, $res->web()->pages()[0]);
+ }
+
+ public function testFaceAndLabelDetection()
+ {
+ $image = $this->client->image(
+ file_get_contents($this->getFixtureFilePath('obama.jpg')) , [
+ 'FACE_DETECTION',
+ 'LABEL_DETECTION'
+ ]
+ );
+
+ $res = $this->client->annotate($image);
+
+ $this->assertInstanceOf(Annotation::class, $res);
+
+ // Face Detection
+ $this->assertInstanceOf(Face::class, $res->faces()[0]);
+ $this->assertInstanceOf(Landmarks::class, $res->faces()[0]->landmarks());
+ $this->assertTrue($res->faces()[0]->isJoyful());
+ $this->assertFalse($res->faces()[0]->isSorrowful());
+ $this->assertFalse($res->faces()[0]->isAngry());
+ $this->assertFalse($res->faces()[0]->isSurprised());
+ $this->assertFalse($res->faces()[0]->isUnderExposed());
+ $this->assertFalse($res->faces()[0]->isBlurred());
+ $this->assertFalse($res->faces()[0]->hasHeadwear());
+
+ // Label Detection
+ $this->assertInstanceOf(Entity::class, $res->labels()[0]);
+ }
+
+ public function testLogoDetection()
+ {
+ $image = $this->client->image(
+ file_get_contents($this->getFixtureFilePath('google.jpg')) , [
+ 'LOGO_DETECTION'
+ ]
+ );
+
+ $res = $this->client->annotate($image);
+ $this->assertInstanceOf(Annotation::class, $res);
+ $this->assertInstanceOf(Entity::class, $res->logos()[0]);
+ $this->assertEquals('Google', $res->logos()[0]->description());
+ }
+
+ public function testTextDetection()
+ {
+ $image = $this->client->image(
+ file_get_contents($this->getFixtureFilePath('text.jpg')) , [
+ 'TEXT_DETECTION'
+ ]
+ );
+
+ $res = $this->client->annotate($image);
+ $this->assertInstanceOf(Annotation::class, $res);
+ $this->assertInstanceOf(Entity::class, $res->text()[0]);
+ $this->assertEquals("Hello World.", explode("\n", $res->text()[0]->description())[0]);
+ $this->assertEquals("Goodby World!", explode("\n", $res->text()[0]->description())[1]);
+ }
+
+ public function testDocumentTextDetection()
+ {
+ $image = $this->client->image(
+ file_get_contents($this->getFixtureFilePath('text.jpg')) , [
+ 'DOCUMENT_TEXT_DETECTION'
+ ]
+ );
+
+ $res = $this->client->annotate($image);
+
+ $this->assertInstanceOf(Annotation::class, $res);
+ $this->assertInstanceOf(Document::class, $res->fullText());
+ }
+}
diff --git a/tests/system/Vision/VisionTestCase.php b/tests/system/Vision/VisionTestCase.php
new file mode 100644
index 000000000000..40fca44dc242
--- /dev/null
+++ b/tests/system/Vision/VisionTestCase.php
@@ -0,0 +1,47 @@
+ $keyFilePath
+ ]);
+ self::$hasSetUp = true;
+ }
+
+ protected function getFixtureFilePath($file)
+ {
+ return __DIR__ .'/fixtures/'. $file;
+ }
+}
diff --git a/tests/system/Vision/fixtures/google.jpg b/tests/system/Vision/fixtures/google.jpg
new file mode 100644
index 000000000000..29834cfede17
Binary files /dev/null and b/tests/system/Vision/fixtures/google.jpg differ
diff --git a/tests/system/Vision/fixtures/landmark.jpg b/tests/system/Vision/fixtures/landmark.jpg
new file mode 100644
index 000000000000..aeae47494078
Binary files /dev/null and b/tests/system/Vision/fixtures/landmark.jpg differ
diff --git a/tests/system/Vision/fixtures/obama.jpg b/tests/system/Vision/fixtures/obama.jpg
new file mode 100644
index 000000000000..e5128d3fe85f
Binary files /dev/null and b/tests/system/Vision/fixtures/obama.jpg differ
diff --git a/tests/system/Vision/fixtures/text.jpg b/tests/system/Vision/fixtures/text.jpg
new file mode 100644
index 000000000000..55f1db964e89
Binary files /dev/null and b/tests/system/Vision/fixtures/text.jpg differ
diff --git a/tests/system/data/table-data.csv b/tests/system/data/table-data.csv
deleted file mode 100644
index 687d1cd433bc..000000000000
--- a/tests/system/data/table-data.csv
+++ /dev/null
@@ -1,2 +0,0 @@
-Detroit,MI
-Ann Arbor,MI
diff --git a/tests/system/data/table-data.json b/tests/system/data/table-data.json
new file mode 100644
index 000000000000..b8321597a98b
--- /dev/null
+++ b/tests/system/data/table-data.json
@@ -0,0 +1,3 @@
+{"Name":"Bilbo","Age":"111","Weight":67.2,"IsMagic":false,"Spells":[],"ImportantDates":{"TeaTime":"10:00:00","NextVacation":"2017-09-22","FavoriteTime":"2031-04-01T05:09:27"},"FavoriteNumbers":[1]}
+{"Name":"Gandalf","Age":"1000","Weight":198.6,"IsMagic":true,"Spells":[{"Name": "Skydragon", "Icon":"iVBORw0KGgoAAAANSUhEUgAAAB4AAAAgCAYAAAAFQMh/AAAAAXNSR0IArs4c6QAAA9lJREFUSA21lk9OVEEQxvsRDImoiMG9mLjjCG5mEg7gEfQGsIcF7p0EDsBBSJiNO7ZsFRZqosb/QkSj7fer7ur33sw8GDFUUq+7q6vqq6qu7pkQzqG4EeI521e7FePVgM9cGPYwhCi6UO8qFOK+YY+Br66ujsmmxb84Yzwp6zCsxjJfWVkxnMsEMGuWHZ9Wcz11cM48hkq0vLwc1tbW4mAwqDpcdIqnMmgF0JMv2CiGnZ2dcHR0FA4PD8Pe3t5U/tx6bCSlb+JT8XfxT3HsUek0Li0tRdjWl+z6iRF+FNA1hXPDQ/IMNyRg3s8bD/OaZS+VP+9cOLSa64cA34oXZWagDkRzAaJxXaE+ufc4rCN7LrazZ2+8+STtpAL8WYDvpTaHKlkB2iQARMvb2+H27m4YaL7zaDtUw1BZAASi6T8T2UZnPZV2pvnJfCH5p8bewcGB6TrIfz8wBZgHQ83kjpuj6RBYQpuo09Tvmpd7TPe+ktZN8cKwS92KWXGuaqWowlYEwthtMcWOZUNJc8at+zuF/Xkqo69baS7P+AvWjYwJ4jyHXXsEnd74ZO/Pq+uXUuv6WNlso6cvnDsZB1V/unJab3D1/KrJDw9NCM9wHf2FK2ejTKMejnBHfGtfH7LGGCdQDqaqJgfgzWjXK1nYV4jRbPGnxUT7cqUaZfJrVZeOm9QmB21L6xXgbu/ScsYusJFMoU0x2fsamRJOd6kOYDRLUxv94ENZe8+0gM+0dyz+KgU7X8rLHHCIOZyrna4y6ykIu0YCs02TBXmk3PZssmEgaTxTo83xjCIjoE21h0Yah3MrV4+9kR8MaabGze+9NEILGAFE5nMOiiA32KnAr/sb7tED3nzlzC4dB38WMC+EjaqHfqvUKHi2gJPdWQ6AbH8hgyQ7QY6jvjj3QZWvX6pUAtduTX5Dss96Q7NI9RQRJeeKvRFbt0v2gb1Gx/PooJsztn1c1DqpAU3Hde2dB2aEHBhjgOFjMeDvxLafjQ3YZQSgOcHJZX611H45sGLHWvYTz9hiURlpNoBZvxb/Ft9lAQ1DmBfUiR+j1hAPkMBTE9L9+zLva1QvGFHurRBaZ5xLVitoBviiRkD/sIMDztKA5FA0b9/0OclzO2/XAQymJ0TcghZwEo9/AX8gMeAJMOvIsWWt5bwCoiFhVSllrdH0t5Q1JHAFlKJNkvTVdn2GHb9KdmacMT+d/Os05imJUccRX2YuZ93Sxf0Ilc4DPDeAq5SAvFEAY94cQc6BA26dzb4HWAJI4DPmQE5KCVUyvb2FcDZem7JdT2ggKUP3xX6n9XNq1DpzSf4Cy4ZqSlmM8d8AAAAASUVORK5CYII=","DiscoveredBy":"Firebreather","Properties":[{"Name":"Flying","Power":1}],"LastUsed":"2015-10-31 23:59:56 UTC"}],"ImportantDates":{"TeaTime":"15:00:00","NextVacation":"2666-06-06","FavoriteTime":"2001-12-19T23:59:59"},"FavoriteNumbers":[3]}
+{"Name":"Sabrina","Age":"17","Weight":128.3,"IsMagic":true,"Spells":[{"Name": "Talking cats", "Icon":"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAABxpRE9UAAAAAgAAAAAAAAAgAAAAKAAAACAAAAAgAAABxj2CfowAAAGSSURBVHgB7Jc9TsNAEIX3JDkCPUV6KlpKFHEGlD4nyA04ACUXQKTgCEipUnKGNEbP0otentayicZ24SlWs7tjO/N9u/5J2b2+NUtuZcnwYE8BuQPyGZAPwXwLLPk5kG+BJa9+fgfkh1B+CeancL4F8i2Q/wWm/S/w+XFoTseftn0dvhu0OXfhpM+AGvzcEiYVAFisPqE9zrETJhHAlXfg2lglMK9z0f3RBfB+ZyRUV3x+erzsEIjjOBqc1xtNAIrvguybV3A9lkVHxlEE6GrrPb/ZvAySwlUnfCmlPQ+R8JCExvGtcRQBLFwj4FGkznX1VYDKPG/f2/MjwCksXACgdNUxJjwK9xwl4JihOwTFR0kIF+CABEPRnvsvPFctMoYKqAFSAFaMwB4pp3Y+bodIYL9WmIAaIOHxo7W8wiHvAjTvhUeNwwSgeAeAABbqOewC5hBdwFD4+9+7puzXV9fS6/b1wwT4tsaYAhwOOQdUQch5vgZCeAhAv3ZM31yYAAUgvApQQQ6n5w6FB/RVe1jdJOAPAAD//1eMQwoAAAGQSURBVO1UMU4DQQy8X9AgWopIUINEkS4VlJQo4gvwAV7AD3gEH4iSgidESpWSXyyZExP5lr0c7K5PsXBhec/2+jzjuWtent9CLdtu1mG5+gjz+WNr7IsY7eH+tvO+xfuqk4vz7CH91edFaF5v9nb6dBKm13edvrL+0Lk5lMzJkQDeJSkkgHF6mR8CHwMHCQR/NAQQGD0BAlwK4FCefQiefq+A2Vn29tG7igLAfmwcnJu/nJy3BMQkMN9HEPr8AL3bfBv7Bp+7/SoExMDjZwKEJwmyhnnmQIQEBIlz2x0iKoAvJkAC6TsTIH6MqRrEWUMSZF2zAwqT4Eu/e6pzFAIkmNSZ4OFT+VYBIIF//UqbJwnF/4DU0GwOn8r/JQYCpPGufEfJuZiA37ycQw/5uFeqPq4pfR6FADmkBCXjfWdZj3NfXW58dAJyB9W65wRoMWulryvAyqa05nQFaDFrpa8rwMqmtOZ0BWgxa6WvK8DKprTmdAVoMWulryvAyqa05nQFaDFrpa8rwMqmtOb89wr4AtQ4aPoL6yVpAAAAAElFTkSuQmCC","DiscoveredBy":"Salem","Properties":[{"Name":"Makes you look crazy","Power":1}],"LastUsed":"2017-02-14 12:07:23 UTC"}],"ImportantDates":{"TeaTime":"12:00:00","NextVacation":"2017-03-14","FavoriteTime":"2000-10-31T23:27:46"},"FavoriteNumbers":[7]}
diff --git a/tests/system/data/table-schema.json b/tests/system/data/table-schema.json
new file mode 100644
index 000000000000..9a676d1a19ae
--- /dev/null
+++ b/tests/system/data/table-schema.json
@@ -0,0 +1,93 @@
+[
+ {
+ "mode": "NULLABLE",
+ "name": "Name",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "Age",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "Weight",
+ "type": "FLOAT"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "IsMagic",
+ "type": "BOOLEAN"
+ },
+ {
+ "fields": [
+ {
+ "mode": "NULLABLE",
+ "name": "Name",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "LastUsed",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "DiscoveredBy",
+ "type": "STRING"
+ },
+ {
+ "fields": [
+ {
+ "mode": "NULLABLE",
+ "name": "Name",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "Power",
+ "type": "FLOAT"
+ }
+ ],
+ "mode": "REPEATED",
+ "name": "Properties",
+ "type": "RECORD"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "Icon",
+ "type": "BYTES"
+ }
+ ],
+ "mode": "REPEATED",
+ "name": "Spells",
+ "type": "RECORD"
+ },
+ {
+ "fields": [
+ {
+ "mode": "NULLABLE",
+ "name": "TeaTime",
+ "type": "TIME"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "NextVacation",
+ "type": "DATE"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "FavoriteTime",
+ "type": "DATETIME"
+ }
+ ],
+ "mode": "NULLABLE",
+ "name": "ImportantDates",
+ "type": "RECORD"
+ },
+ {
+ "mode": "REPEATED",
+ "name": "FavoriteNumbers",
+ "type": "INTEGER"
+ }
+]
diff --git a/tests/unit/BigQuery/BigQueryClientTest.php b/tests/unit/BigQuery/BigQueryClientTest.php
index ad45055fd860..b4030ff4f374 100644
--- a/tests/unit/BigQuery/BigQueryClientTest.php
+++ b/tests/unit/BigQuery/BigQueryClientTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\BigQuery\Bytes;
diff --git a/tests/unit/BigQuery/BytesTest.php b/tests/unit/BigQuery/BytesTest.php
index 66e446c00c9f..37865bf9eafd 100644
--- a/tests/unit/BigQuery/BytesTest.php
+++ b/tests/unit/BigQuery/BytesTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Bytes;
use GuzzleHttp\Psr7;
diff --git a/tests/unit/BigQuery/Connection/RestTest.php b/tests/unit/BigQuery/Connection/RestTest.php
index 50ebeba9a383..af3a26b5bcad 100644
--- a/tests/unit/BigQuery/Connection/RestTest.php
+++ b/tests/unit/BigQuery/Connection/RestTest.php
@@ -15,10 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery\Connection;
+namespace Google\Cloud\Tests\Unit\BigQuery\Connection;
+use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\BigQuery\Connection\Rest;
-use Google\Cloud\Upload\AbstractUploader;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\Upload\AbstractUploader;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
@@ -35,7 +38,7 @@ class RestTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
- $this->requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper');
+ $this->requestWrapper = $this->prophesize(RequestWrapper::class);
$this->successBody = '{"canI":"kickIt"}';
}
@@ -48,7 +51,7 @@ public function testCallBasicMethods($method)
$request = new Request('GET', '/somewhere');
$response = new Response(200, [], $this->successBody);
- $requestBuilder = $this->prophesize('Google\Cloud\RequestBuilder');
+ $requestBuilder = $this->prophesize(RequestBuilder::class);
$requestBuilder->build(
Argument::type('string'),
Argument::type('string'),
@@ -56,7 +59,7 @@ public function testCallBasicMethods($method)
)->willReturn($request);
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->willReturn($response);
@@ -115,7 +118,7 @@ public function testInsertJobUpload()
]
]));
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->will(
function ($args) use (&$actualRequest, $response) {
diff --git a/tests/unit/BigQuery/DatasetTest.php b/tests/unit/BigQuery/DatasetTest.php
index ead3bcf574e4..1f9703811f96 100644
--- a/tests/unit/BigQuery/DatasetTest.php
+++ b/tests/unit/BigQuery/DatasetTest.php
@@ -15,12 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
use Google\Cloud\BigQuery\Dataset;
use Google\Cloud\BigQuery\Table;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\BigQuery\ValueMapper;
+use Google\Cloud\Core\Exception\NotFoundException;
use Prophecy\Argument;
/**
@@ -29,18 +30,26 @@
class DatasetTest extends \PHPUnit_Framework_TestCase
{
public $connection;
+ public $mapper;
public $projectId = 'myProjectId';
public $datasetId = 'myDatasetId';
public $tableId = 'myTableId';
public function setUp()
{
+ $this->mapper = new ValueMapper(false);
$this->connection = $this->prophesize(ConnectionInterface::class);
}
public function getDataset($connection, array $data = [])
{
- return new Dataset($connection->reveal(), $this->datasetId, $this->projectId, $data);
+ return new Dataset(
+ $connection->reveal(),
+ $this->datasetId,
+ $this->projectId,
+ $this->mapper,
+ $data
+ );
}
public function testDoesExistTrue()
diff --git a/tests/unit/BigQuery/DateTest.php b/tests/unit/BigQuery/DateTest.php
index 6e28a8f8449d..a7479819619b 100644
--- a/tests/unit/BigQuery/DateTest.php
+++ b/tests/unit/BigQuery/DateTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Date;
diff --git a/tests/unit/BigQuery/InsertResponseTest.php b/tests/unit/BigQuery/InsertResponseTest.php
index c411a3b53e1a..2081d0d33b74 100644
--- a/tests/unit/BigQuery/InsertResponseTest.php
+++ b/tests/unit/BigQuery/InsertResponseTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\InsertResponse;
use Prophecy\Argument;
diff --git a/tests/unit/BigQuery/JobTest.php b/tests/unit/BigQuery/JobTest.php
index 8613fa90ff3e..5bbcf5251b2a 100644
--- a/tests/unit/BigQuery/JobTest.php
+++ b/tests/unit/BigQuery/JobTest.php
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
use Google\Cloud\BigQuery\Job;
use Google\Cloud\BigQuery\QueryResults;
use Google\Cloud\BigQuery\ValueMapper;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Prophecy\Argument;
/**
diff --git a/tests/unit/BigQuery/QueryResultsTest.php b/tests/unit/BigQuery/QueryResultsTest.php
index 8e642b5e3e4b..8f655d3a71aa 100644
--- a/tests/unit/BigQuery/QueryResultsTest.php
+++ b/tests/unit/BigQuery/QueryResultsTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
use Google\Cloud\BigQuery\QueryResults;
@@ -63,7 +63,7 @@ public function getQueryResults($connection, array $data = [])
}
/**
- * @expectedException \Google\Cloud\Exception\GoogleException
+ * @expectedException \Google\Cloud\Core\Exception\GoogleException
*/
public function testGetsRowsThrowsExceptionWhenQueryNotComplete()
{
diff --git a/tests/unit/BigQuery/TableTest.php b/tests/unit/BigQuery/TableTest.php
index 3051dd0c2d9b..7082d2cc8466 100644
--- a/tests/unit/BigQuery/TableTest.php
+++ b/tests/unit/BigQuery/TableTest.php
@@ -15,16 +15,17 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
use Google\Cloud\BigQuery\InsertResponse;
use Google\Cloud\BigQuery\Job;
use Google\Cloud\BigQuery\Table;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\BigQuery\ValueMapper;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Core\Upload\AbstractUploader;
use Google\Cloud\Storage\Connection\ConnectionInterface as StorageConnectionInterface;
use Google\Cloud\Storage\StorageObject;
-use Google\Cloud\Upload\AbstractUploader;
use Prophecy\Argument;
/**
@@ -34,6 +35,7 @@ class TableTest extends \PHPUnit_Framework_TestCase
{
public $connection;
public $storageConnection;
+ public $mapper;
public $fileName = 'myfile.csv';
public $bucketName = 'myBucket';
public $projectId = 'myProjectId';
@@ -46,7 +48,12 @@ class TableTest extends \PHPUnit_Framework_TestCase
];
public $schemaData = [
'schema' => [
- 'fields' => [['name' => 'first_name']]
+ 'fields' => [
+ [
+ 'name' => 'first_name',
+ 'type' => 'STRING'
+ ]
+ ]
]
];
public $insertJobResponse = [
@@ -57,6 +64,7 @@ class TableTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
+ $this->mapper = new ValueMapper(false);
$this->connection = $this->prophesize(ConnectionInterface::class);
$this->storageConnection = $this->prophesize(StorageConnectionInterface::class);
}
@@ -77,6 +85,7 @@ public function getTable($connection, array $data = [], $tableId = null)
$tableId ?: $this->tableId,
$this->datasetId,
$this->projectId,
+ $this->mapper,
$data
);
}
@@ -208,9 +217,11 @@ public function testRunsCopyJob()
$this->assertEquals($this->insertJobResponse, $job->info());
}
- public function testRunsExportJob()
+ /**
+ * @dataProvider destinationProvider
+ */
+ public function testRunsExportJob($destinationObject)
{
- $destinationObject = $this->getObject();
$expectedArguments = [
'projectId' => $this->projectId,
'configuration' => [
@@ -236,6 +247,20 @@ public function testRunsExportJob()
$this->assertEquals($this->insertJobResponse, $job->info());
}
+ public function destinationProvider()
+ {
+ $this->setUp();
+
+ return [
+ [$this->getObject()],
+ [sprintf(
+ 'gs://%s/%s',
+ $this->bucketName,
+ $this->fileName
+ )]
+ ];
+ }
+
public function testRunsLoadJob()
{
$data = 'abc';
diff --git a/tests/unit/BigQuery/TimeTest.php b/tests/unit/BigQuery/TimeTest.php
index b26681473b1e..72cba3ec585d 100644
--- a/tests/unit/BigQuery/TimeTest.php
+++ b/tests/unit/BigQuery/TimeTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Time;
diff --git a/tests/unit/BigQuery/TimestampTest.php b/tests/unit/BigQuery/TimestampTest.php
index e918bf393f8c..a0f47795289c 100644
--- a/tests/unit/BigQuery/TimestampTest.php
+++ b/tests/unit/BigQuery/TimestampTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Timestamp;
diff --git a/tests/unit/BigQuery/ValueMapperTest.php b/tests/unit/BigQuery/ValueMapperTest.php
index 1d8374803f44..4b211f8a294a 100644
--- a/tests/unit/BigQuery/ValueMapperTest.php
+++ b/tests/unit/BigQuery/ValueMapperTest.php
@@ -15,14 +15,14 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\BigQuery;
+namespace Google\Cloud\Tests\Unit\BigQuery;
use Google\Cloud\BigQuery\Bytes;
use Google\Cloud\BigQuery\Date;
use Google\Cloud\BigQuery\Time;
use Google\Cloud\BigQuery\Timestamp;
use Google\Cloud\BigQuery\ValueMapper;
-use Google\Cloud\Int64;
+use Google\Cloud\Core\Int64;
/**
* @group bigquery
@@ -134,10 +134,25 @@ public function bigQueryValueProvider()
new Time(new \DateTime('12:15:15'))
],
[
- ['v' => '1438712914'],
+ ['v' => '1.438712914E9'],
['type' => 'TIMESTAMP'],
new Timestamp(new \DateTime('2015-08-04 18:28:34Z'))
],
+ [
+ ['v' => '2678400.0'],
+ ['type' => 'TIMESTAMP'],
+ new Timestamp(new \DateTime('1970-02-01'))
+ ],
+ [
+ ['v' => '-3.1561919984985E8'],
+ ['type' => 'TIMESTAMP'],
+ new Timestamp(new \DateTime('1960-01-01 00:00:00.150150Z'))
+ ],
+ [
+ ['v' => '9.4668480015015E8'],
+ ['type' => 'TIMESTAMP'],
+ new Timestamp(new \DateTime('2000-01-01 00:00:00.150150Z'))
+ ],
[
[
'v' => [
@@ -201,6 +216,34 @@ public function testMapsBytesFromBigQuery()
$this->assertEquals((string) new Bytes('abcd'), (string) $actual);
}
+ /**
+ * @dataProvider toBigQueryValueProvider
+ */
+ public function testMapsToBigQuery($value, $expected)
+ {
+ $mapper = new ValueMapper(false);
+ $actual = $mapper->toBigQuery($value);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function toBigQueryValueProvider()
+ {
+ $dt = new \DateTime();
+ $date = new Date($dt);
+ $int64 = new Int64('123');
+
+ return [
+ [$dt, $dt->format('Y-m-d\TH:i:s.u')],
+ [$date, (string) $date],
+ [
+ ['date' => $date],
+ ['date' => (string) $date]
+ ],
+ [1, 1]
+ ];
+ }
+
/**
* @dataProvider parameterValueProvider
*/
diff --git a/tests/unit/ArrayTraitTest.php b/tests/unit/Core/ArrayTraitTest.php
similarity index 74%
rename from tests/unit/ArrayTraitTest.php
rename to tests/unit/Core/ArrayTraitTest.php
index 53dc38667917..2577051fd200 100644
--- a/tests/unit/ArrayTraitTest.php
+++ b/tests/unit/Core/ArrayTraitTest.php
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\ArrayTrait;
+use Google\Cloud\Core\ArrayTrait;
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class ArrayTraitTest extends \PHPUnit_Framework_TestCase
{
@@ -83,6 +83,26 @@ public function testIsAssocFalse()
$this->assertFalse($actual);
}
+
+ public function testArrayFilterRemoveNull()
+ {
+ $input = [
+ 'null' => null,
+ 'false' => false,
+ 'zero' => 0,
+ 'float' => 0.0,
+ 'empty' => '',
+ 'array' => [],
+ ];
+
+ $res = $this->implementation->call('arrayFilterRemoveNull', [$input]);
+ $this->assertFalse(array_key_exists('null', $res));
+ $this->assertTrue(array_key_exists('false', $res));
+ $this->assertTrue(array_key_exists('zero', $res));
+ $this->assertTrue(array_key_exists('float', $res));
+ $this->assertTrue(array_key_exists('empty', $res));
+ $this->assertTrue(array_key_exists('array', $res));
+ }
}
class ArrayTraitStub
diff --git a/tests/unit/CallTraitTest.php b/tests/unit/Core/CallTraitTest.php
similarity index 93%
rename from tests/unit/CallTraitTest.php
rename to tests/unit/Core/CallTraitTest.php
index b17fa31165bb..6ae6842ca25a 100644
--- a/tests/unit/CallTraitTest.php
+++ b/tests/unit/Core/CallTraitTest.php
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Test;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\CallTrait;
+use Google\Cloud\Core\CallTrait;
/**
- * @group root
+ * @group core
*/
class CallTraitTest extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/ClientTraitTest.php b/tests/unit/Core/ClientTraitTest.php
similarity index 88%
rename from tests/unit/ClientTraitTest.php
rename to tests/unit/Core/ClientTraitTest.php
index 4e581f3e9a3e..d79f04120472 100644
--- a/tests/unit/ClientTraitTest.php
+++ b/tests/unit/Core/ClientTraitTest.php
@@ -15,19 +15,19 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\ClientTrait;
-use Google\Cloud\Compute\Metadata;
+use Google\Cloud\Core\ClientTrait;
+use Google\Cloud\Core\Compute\Metadata;
use GuzzleHttp\Psr7\Response;
/**
- * @group root
+ * @group core
*/
class ClientTraitTest extends \PHPUnit_Framework_TestCase
{
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testGetConnectionTypeThrowsExceptionWhenAttempingGrpcWithoutDeps()
{
@@ -69,7 +69,7 @@ public function dependencyStatusProvider()
public function testConfigureAuthentication()
{
- $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json';
+ $keyFilePath = __DIR__ . '/../fixtures/json-key-fixture.json';
putenv("GOOGLE_APPLICATION_CREDENTIALS=$keyFilePath"); // for application default credentials
$trait = new ClientTraitStub;
@@ -81,7 +81,7 @@ public function testConfigureAuthentication()
public function testConfigureAuthenticationWithKeyFile()
{
- $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json';
+ $keyFilePath = __DIR__ . '/../fixtures/json-key-fixture.json';
$keyFile = json_decode(file_get_contents($keyFilePath), true);
$keyFile['project_id'] = 'test';
@@ -96,7 +96,7 @@ public function testConfigureAuthenticationWithKeyFile()
public function testConfigureAuthenticationWithKeyFilePath()
{
- $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json';
+ $keyFilePath = __DIR__ . '/../fixtures/json-key-fixture.json';
$keyFile = json_decode(file_get_contents($keyFilePath), true);
$trait = new ClientTraitStub;
@@ -109,7 +109,7 @@ public function testConfigureAuthenticationWithKeyFilePath()
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testConfigureAuthenticationWithInvalidKeyFilePath()
{
@@ -122,7 +122,7 @@ public function testConfigureAuthenticationWithInvalidKeyFilePath()
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testConfigureAuthenticationWithKeyFileThatCantBeDecoded()
{
@@ -135,11 +135,11 @@ public function testConfigureAuthenticationWithKeyFileThatCantBeDecoded()
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testDetectProjectIdWithNoProjectIdAvailable()
{
- $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json';
+ $keyFilePath = __DIR__ . '/../fixtures/json-key-fixture.json';
$keyFile = json_decode(file_get_contents($keyFilePath), true);
unset($keyFile['project_id']);
@@ -167,7 +167,7 @@ public function testDetectProjectIdOnGce()
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testDetectProjectIdOnGceButOhNoThereStillIsntAProjectId()
{
diff --git a/tests/unit/Compute/MetadataTest.php b/tests/unit/Core/Compute/MetadataTest.php
similarity index 92%
rename from tests/unit/Compute/MetadataTest.php
rename to tests/unit/Core/Compute/MetadataTest.php
index 599d20c09f4d..6b939bac9f28 100644
--- a/tests/unit/Compute/MetadataTest.php
+++ b/tests/unit/Core/Compute/MetadataTest.php
@@ -15,11 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Compute;
+namespace Google\Cloud\Tests\Unit\Core\Compute;
-use Google\Cloud\Compute\Metadata;
+use Google\Cloud\Core\Compute\Metadata;
+use Google\Cloud\Core\Compute\Metadata\Readers\StreamReader;
/**
+ * @group core
* @group compute
*/
class MetadataTest extends \PHPUnit_Framework_TestCase
@@ -31,7 +33,7 @@ public function setUp()
{
$this->metadata = new Metadata();
$this->mock = $this->getMockBuilder(
- '\Google\Cloud\Compute\Metadata\Readers\StreamReader')
+ StreamReader::class)
->setMethods(array('read'))
->getmock();
$this->metadata->setReader($this->mock);
diff --git a/tests/unit/EmulatorTraitTest.php b/tests/unit/Core/EmulatorTraitTest.php
similarity index 93%
rename from tests/unit/EmulatorTraitTest.php
rename to tests/unit/Core/EmulatorTraitTest.php
index 13cc5767190b..e0ae80ec0fbb 100644
--- a/tests/unit/EmulatorTraitTest.php
+++ b/tests/unit/Core/EmulatorTraitTest.php
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\EmulatorTrait;
+use Google\Cloud\Core\EmulatorTrait;
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class EmulatorTraitTest extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/Exception/ServiceExceptionTest.php b/tests/unit/Core/Exception/ServiceExceptionTest.php
similarity index 90%
rename from tests/unit/Exception/ServiceExceptionTest.php
rename to tests/unit/Core/Exception/ServiceExceptionTest.php
index aa4554a182ab..cec5d5c85be0 100644
--- a/tests/unit/Exception/ServiceExceptionTest.php
+++ b/tests/unit/Core/Exception/ServiceExceptionTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Exception;
+namespace Google\Cloud\Tests\Unit\Core\Exception;
-use Google\Cloud\Exception\ServiceException;
+use Google\Cloud\Core\Exception\ServiceException;
/**
+ * @group core
* @group exception
*/
class ServiceExceptionTest extends \PHPUnit_Framework_TestCase
diff --git a/tests/unit/ExponentialBackoffTest.php b/tests/unit/Core/ExponentialBackoffTest.php
similarity index 97%
rename from tests/unit/ExponentialBackoffTest.php
rename to tests/unit/Core/ExponentialBackoffTest.php
index 884948172ece..8971a38bbf5b 100644
--- a/tests/unit/ExponentialBackoffTest.php
+++ b/tests/unit/Core/ExponentialBackoffTest.php
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\ExponentialBackoff;
+use Google\Cloud\Core\ExponentialBackoff;
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class ExponentialBackoffTest extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/GrpcRequestWrapperTest.php b/tests/unit/Core/GrpcRequestWrapperTest.php
similarity index 84%
rename from tests/unit/GrpcRequestWrapperTest.php
rename to tests/unit/Core/GrpcRequestWrapperTest.php
index e8dc9014246d..11243cb17ab5 100644
--- a/tests/unit/GrpcRequestWrapperTest.php
+++ b/tests/unit/Core/GrpcRequestWrapperTest.php
@@ -16,18 +16,19 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
use DrSlump\Protobuf\Message;
-use Google\Cloud\Exception;
-use Google\Cloud\GrpcRequestWrapper;
+use Google\Auth\FetchAuthTokenInterface;
+use Google\Cloud\Core\Exception;
+use Google\Cloud\Core\GrpcRequestWrapper;
use Google\GAX\ApiException;
use Google\GAX\Page;
use Google\GAX\PagedListResponse;
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class GrpcRequestWrapperTest extends \PHPUnit_Framework_TestCase
{
@@ -44,12 +45,17 @@ public function setUp()
public function testSuccessfullySendsRequest($response, $expectedMessage)
{
$requestWrapper = new GrpcRequestWrapper();
+ $requestOptions = [
+ 'requestTimeout' => 3.5
+ ];
$actualResponse = $requestWrapper->send(
- function ($test) use ($response) {
+ function ($test, $options) use ($response, $requestOptions) {
+ $this->assertEquals($requestOptions['requestTimeout'] * 1000, $options['timeoutMs']);
return $response;
},
- ['test', []]
+ ['test', []],
+ $requestOptions
);
$this->assertEquals($expectedMessage, $actualResponse);
@@ -73,7 +79,7 @@ public function responseProvider()
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testThrowsExceptionWhenRequestFails()
{
@@ -104,7 +110,7 @@ public function testCredentialsFetcher($wrapperConfig)
$requestWrapper = new GrpcRequestWrapper($wrapperConfig);
$this->assertInstanceOf(
- 'Google\Auth\FetchAuthTokenInterface',
+ FetchAuthTokenInterface::class,
$requestWrapper->getCredentialsFetcher()
);
}
@@ -119,15 +125,15 @@ public function testCredentialsFromKeyFileStreamCanBeReadMultipleTimes($wrapperC
$requestWrapper->getCredentialsFetcher();
$credentials = $requestWrapper->getCredentialsFetcher();
- $this->assertInstanceOf('Google\Auth\FetchAuthTokenInterface', $credentials);
+ $this->assertInstanceOf(FetchAuthTokenInterface::class, $credentials);
}
public function credentialsProvider()
{
- $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json';
+ $keyFilePath = __DIR__ . '/../fixtures/json-key-fixture.json';
putenv("GOOGLE_APPLICATION_CREDENTIALS=$keyFilePath"); // for application default credentials
- $credentialsFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface');
+ $credentialsFetcher = $this->prophesize(FetchAuthTokenInterface::class);
return [
[['keyFile' => json_decode(file_get_contents($keyFilePath), true)]], // keyFile
@@ -139,7 +145,7 @@ public function credentialsProvider()
public function keyFileCredentialsProvider()
{
- $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json';
+ $keyFilePath = __DIR__ . '/../fixtures/json-key-fixture.json';
return [
[['keyFile' => json_decode(file_get_contents($keyFilePath), true)]], // keyFile
diff --git a/tests/unit/GrpcTraitTest.php b/tests/unit/Core/GrpcTraitTest.php
similarity index 85%
rename from tests/unit/GrpcTraitTest.php
rename to tests/unit/Core/GrpcTraitTest.php
index 0be14e92e096..5bc792fa3c21 100644
--- a/tests/unit/GrpcTraitTest.php
+++ b/tests/unit/Core/GrpcTraitTest.php
@@ -15,18 +15,17 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
use Google\Auth\Cache\MemoryCacheItemPool;
use Google\Auth\FetchAuthTokenCache;
use Google\Auth\FetchAuthTokenInterface;
-use Google\Cloud\GrpcRequestWrapper;
-use Google\Cloud\GrpcTrait;
-use Google\Cloud\ServiceBuilder;
+use Google\Cloud\Core\GrpcRequestWrapper;
+use Google\Cloud\Core\GrpcTrait;
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class GrpcTraitTest extends \PHPUnit_Framework_TestCase
{
@@ -63,19 +62,43 @@ public function testSendsRequest()
$this->assertEquals($message, $actualResponse);
}
+ public function testSendsRequestWithOptions()
+ {
+ $options = [
+ 'requestTimeout' => 3.5,
+ 'grpcOptions' => ['timeoutMs' => 100],
+ 'retries' => 0
+ ];
+ $message = ['successful' => 'message'];
+ $this->requestWrapper->send(
+ Argument::type('callable'),
+ Argument::type('array'),
+ $options
+ )->willReturn($message);
+
+ $this->implementation->setRequestWrapper($this->requestWrapper->reveal());
+ $actualResponse = $this->implementation->send(function () {
+ return true;
+ }, [$options]);
+
+ $this->assertEquals($message, $actualResponse);
+ }
+
public function testGetsGaxConfig()
{
+ $version = '1.0.0';
+
$fetcher = $this->prophesize(FetchAuthTokenInterface::class)->reveal();
$this->requestWrapper->getCredentialsFetcher()->willReturn($fetcher);
$this->implementation->setRequestWrapper($this->requestWrapper->reveal());
$expected = [
'credentialsLoader' => $fetcher,
'enableCaching' => false,
- 'appName' => 'gcloud-php',
- 'appVersion' => ServiceBuilder::VERSION
+ 'libName' => 'gccl',
+ 'libVersion' => $version
];
- $this->assertEquals($expected, $this->implementation->call('getGaxConfig'));
+ $this->assertEquals($expected, $this->implementation->call('getGaxConfig', [$version]));
}
public function testFormatsTimestamp()
diff --git a/tests/unit/Iam/IamTest.php b/tests/unit/Core/Iam/IamTest.php
similarity index 96%
rename from tests/unit/Iam/IamTest.php
rename to tests/unit/Core/Iam/IamTest.php
index 16262c9e0d44..58d9218a3469 100644
--- a/tests/unit/Iam/IamTest.php
+++ b/tests/unit/Core/Iam/IamTest.php
@@ -15,13 +15,14 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Iam;
+namespace Google\Cloud\Tests\Unit\Core\Iam;
-use Google\Cloud\Iam\Iam;
-use Google\Cloud\Iam\IamConnectionInterface;
+use Google\Cloud\Core\Iam\Iam;
+use Google\Cloud\Core\Iam\IamConnectionInterface;
use Prophecy\Argument;
/**
+ * @group core
* @group iam
*/
class IamTest extends \PHPUnit_Framework_TestCase
diff --git a/tests/unit/Iam/PolicyBuilderTest.php b/tests/unit/Core/Iam/PolicyBuilderTest.php
similarity index 97%
rename from tests/unit/Iam/PolicyBuilderTest.php
rename to tests/unit/Core/Iam/PolicyBuilderTest.php
index cf411eb3bb6f..e271bae45923 100644
--- a/tests/unit/Iam/PolicyBuilderTest.php
+++ b/tests/unit/Core/Iam/PolicyBuilderTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Iam;
+namespace Google\Cloud\Tests\Unit\Core\Iam;
-use Google\Cloud\Iam\PolicyBuilder;
+use Google\Cloud\Core\Iam\PolicyBuilder;
/**
+ * @group core
* @group iam
*/
class PolicyBuilderTest extends \PHPUnit_Framework_TestCase
diff --git a/tests/unit/Int64Test.php b/tests/unit/Core/Int64Test.php
similarity index 92%
rename from tests/unit/Int64Test.php
rename to tests/unit/Core/Int64Test.php
index 5bf9b7626ac1..770a1b3e8e4e 100644
--- a/tests/unit/Int64Test.php
+++ b/tests/unit/Core/Int64Test.php
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\Int64;
+use Google\Cloud\Core\Int64;
/**
- * @group root
+ * @group core
*/
class Int64Test extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/Core/Iterator/ItemIteratorTest.php b/tests/unit/Core/Iterator/ItemIteratorTest.php
new file mode 100644
index 000000000000..fb9f106aceab
--- /dev/null
+++ b/tests/unit/Core/Iterator/ItemIteratorTest.php
@@ -0,0 +1,79 @@
+prophesize(PageIterator::class);
+ $pageIterator->rewind()
+ ->willReturn(null)
+ ->shouldBeCalledTimes(1);
+ $pageIterator->nextResultToken()
+ ->willReturn('abc', null)
+ ->shouldBeCalledTimes(2);
+ $pageIterator->current()
+ ->willReturn($page1)
+ ->shouldBeCalledTimes(19);
+ $pageIterator->next()
+ ->will(function () use ($page2) {
+ $this->current()->willReturn($page2);
+ })
+ ->shouldBeCalledTimes(1);
+
+ $items = new ItemIterator($pageIterator->reveal());
+
+ $actualItems = [];
+ foreach ($items as $key => $item) {
+ $actualItems[] = $item;
+ }
+
+ $this->assertEquals(array_merge($page1, $page2), $actualItems);
+ }
+
+ public function testGetsNextResultToken()
+ {
+ $nextResultToken = 'abc';
+ $pageIterator = $this->prophesize(PageIterator::class);
+ $pageIterator->nextResultToken()
+ ->willReturn($nextResultToken)
+ ->shouldBeCalledTimes(1);
+
+ $items = new ItemIterator($pageIterator->reveal());
+
+ $this->assertEquals($nextResultToken, $items->nextResultToken());
+ }
+
+ public function testGetsPageIterator()
+ {
+ $pageIterator = $this->prophesize(PageIterator::class);
+
+ $items = new ItemIterator($pageIterator->reveal());
+
+ $this->assertInstanceOf(PageIterator::class, $items->iterateByPage());
+ }
+}
diff --git a/tests/unit/Core/Iterator/PageIteratorTest.php b/tests/unit/Core/Iterator/PageIteratorTest.php
new file mode 100644
index 000000000000..8dbc6c41f86e
--- /dev/null
+++ b/tests/unit/Core/Iterator/PageIteratorTest.php
@@ -0,0 +1,167 @@
+ 'items', 'multiPage' => true]
+ );
+
+ $page1BeforeRewind = iterator_to_array($pages)[0];
+ $pages->rewind();
+ $page1AfterRewind = iterator_to_array($pages)[0];
+
+ $this->assertEquals($page1BeforeRewind, $page1AfterRewind);
+ }
+
+ /**
+ * @dataProvider iteratorDataProvider
+ */
+ public function testIteratesData(array $options, array $iteratorOptions, $expected)
+ {
+ $pages = new PageIterator(
+ function ($result) {
+ return strtoupper($result);
+ },
+ [$this, 'theCall'],
+ $options,
+ $iteratorOptions
+ );
+
+ $pagesArray = iterator_to_array($pages);
+
+ $this->assertEquals($expected, $pagesArray);
+ }
+
+ public function iteratorDataProvider()
+ {
+ return [
+ [
+ ['itemsKey' => 'items'],
+ [],
+ [array_map('strtoupper', self::$page1)]
+ ],
+ [
+ ['itemsKey' => 'tests'],
+ ['itemsKey' => 'tests'],
+ [array_map('strtoupper', self::$page1)]
+ ],
+ [
+ [
+ 'itemsKey' => 'items',
+ 'nextResultTokenKey' => 'nr',
+ 'resultTokenKey' => 'r'
+ ],
+ [
+ 'nextResultTokenKey' => 'nr',
+ 'resultTokenKey' => 'r',
+ 'firstPage' => [
+ 'nr' => 'abc',
+ 'items' => self::$page1
+ ]
+ ],
+ [
+ array_map('strtoupper', self::$page1),
+ array_map('strtoupper', self::$page2)
+ ]
+ ],
+ [
+ [
+ 'itemsKey' => 'items',
+ 'multiPage' => true
+ ],
+ [],
+ [
+ array_map('strtoupper', self::$page1),
+ array_map('strtoupper', self::$page2)
+ ]
+ ],
+ [
+ [
+ 'itemsKey' => 'items',
+ 'multiPage' => true
+ ],
+ [
+ 'setNextResultTokenCondition' => function () {
+ return false;
+ }
+ ],
+ [
+ array_map('strtoupper', self::$page1)
+ ]
+ ],
+ [
+ [
+ 'itemsKey' => 'items',
+ 'multiPage' => true
+ ],
+ ['resultLimit' => 4],
+ [
+ array_map('strtoupper', self::$page1),
+ array_slice(array_map('strtoupper', self::$page2), 0, 1)
+ ]
+ ]
+ ];
+ }
+
+ public function theCall(array $options)
+ {
+ $options += [
+ 'itemsKey' => 'items',
+ 'nextResultTokenKey' => 'nextPageToken',
+ 'resultTokenKey' => 'pageToken'
+ ];
+
+ if (isset($options[$options['resultTokenKey']])) {
+ return [
+ $options['itemsKey'] => self::$page2
+ ];
+ }
+
+ if (isset($options['firstPage'])) {
+
+ }
+
+ if (isset($options['multiPage'])) {
+ return [
+ $options['itemsKey'] => self::$page1,
+ $options['nextResultTokenKey'] => 'abc'
+ ];
+ }
+
+ return [
+ $options['itemsKey'] => self::$page1
+ ];
+ }
+}
diff --git a/tests/unit/Core/JsonTraitTest.php b/tests/unit/Core/JsonTraitTest.php
new file mode 100644
index 000000000000..19e169658db7
--- /dev/null
+++ b/tests/unit/Core/JsonTraitTest.php
@@ -0,0 +1,69 @@
+implementation = new JsonTraitStub();
+ }
+
+ public function testJsonEncode()
+ {
+ $this->assertEquals('10', $this->implementation->call('jsonEncode', [10]));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testJsonEncodeThrowsException()
+ {
+ $this->implementation->call('jsonEncode', [fopen('php://temp', 'r')]);
+ }
+
+ public function testJsonDecode()
+ {
+ $this->assertEquals(10, $this->implementation->call('jsonDecode', ['10']));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testJsonDecodeThrowsException()
+ {
+ $this->implementation->call('jsonDecode', ['.|.']);
+ }
+}
+
+class JsonTraitStub
+{
+ use JsonTrait;
+
+ public function call($fn, array $args)
+ {
+ return call_user_func_array([$this, $fn], $args);
+ }
+}
diff --git a/tests/unit/Logger/AppEngineFlexHandlerTest.php b/tests/unit/Core/Logger/AppEngineFlexHandlerTest.php
similarity index 93%
rename from tests/unit/Logger/AppEngineFlexHandlerTest.php
rename to tests/unit/Core/Logger/AppEngineFlexHandlerTest.php
index 9222cce2ef70..4ea7e0fa76e1 100644
--- a/tests/unit/Logger/AppEngineFlexHandlerTest.php
+++ b/tests/unit/Core/Logger/AppEngineFlexHandlerTest.php
@@ -15,12 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logger;
+namespace Google\Cloud\Tests\Unit\Core\Logger;
-use Google\Cloud\Logger\AppEngineFlexHandler;
+use Google\Cloud\Core\Logger\AppEngineFlexHandler;
use Monolog\Logger;
/**
+ * @group core
* @group logger
*/
class AppEngineFlexHandlerTest extends \PHPUnit_Framework_TestCase
diff --git a/tests/unit/PhpArrayTest.php b/tests/unit/Core/PhpArrayTest.php
similarity index 95%
rename from tests/unit/PhpArrayTest.php
rename to tests/unit/Core/PhpArrayTest.php
index 4df80c571925..1bb285b5190e 100644
--- a/tests/unit/PhpArrayTest.php
+++ b/tests/unit/Core/PhpArrayTest.php
@@ -15,20 +15,20 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
use DrSlump\Protobuf\Message;
-use Google\Cloud\PhpArray;
+use Google\Cloud\Core\PhpArray;
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class PhpArrayTest extends \PHPUnit_Framework_TestCase
{
private function getCodec($customFilters = [])
{
- return new PhpArray($customFilters);
+ return new PhpArray(['customFilters' => $customFilters]);
}
/**
@@ -119,7 +119,7 @@ public static function descriptor()
$f->name = "test_labels";
$f->type = \DrSlump\Protobuf::TYPE_MESSAGE;
$f->rule = \DrSlump\Protobuf::RULE_REPEATED;
- $f->reference = '\Google\Cloud\Tests\TestLabelsEntry';
+ $f->reference = '\Google\Cloud\Tests\Unit\Core\TestLabelsEntry';
$descriptor->addField($f);
$f = new \DrSlump\Protobuf\Field();
diff --git a/tests/unit/RequestBuilderTest.php b/tests/unit/Core/RequestBuilderTest.php
similarity index 91%
rename from tests/unit/RequestBuilderTest.php
rename to tests/unit/Core/RequestBuilderTest.php
index 36831d4d5ac5..7d7d8ce66497 100644
--- a/tests/unit/RequestBuilderTest.php
+++ b/tests/unit/Core/RequestBuilderTest.php
@@ -15,20 +15,20 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\RequestBuilder;
+use Google\Cloud\Core\RequestBuilder;
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class RequestBuilderTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->builder = new RequestBuilder(
- __DIR__ . '/fixtures/service-fixture.json',
+ __DIR__ . '/../fixtures/service-fixture.json',
'http://www.example.com/'
);
}
@@ -52,7 +52,7 @@ public function testBuildsRequest()
public function testBuildsNestedRequest()
{
$builder = new RequestBuilder(
- __DIR__ . '/fixtures/service-fixture.json',
+ __DIR__ . '/../fixtures/service-fixture.json',
'http://www.example.com/',
['resources', 'projects', 'otherThing']
);
@@ -74,7 +74,7 @@ public function testBuildsNestedRequest()
public function testBuildsNestedRequestWithStringSplitting()
{
$builder = new RequestBuilder(
- __DIR__ . '/fixtures/service-fixture.json',
+ __DIR__ . '/../fixtures/service-fixture.json',
'http://www.example.com/',
['resources', 'projects', 'otherThing']
);
diff --git a/tests/unit/RequestWrapperTest.php b/tests/unit/Core/RequestWrapperTest.php
similarity index 75%
rename from tests/unit/RequestWrapperTest.php
rename to tests/unit/Core/RequestWrapperTest.php
index e348b2361384..517e5aa8a36f 100644
--- a/tests/unit/RequestWrapperTest.php
+++ b/tests/unit/Core/RequestWrapperTest.php
@@ -16,9 +16,10 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\RequestWrapper;
+use Google\Auth\FetchAuthTokenInterface;
+use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\ServiceBuilder;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
@@ -26,31 +27,40 @@
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class RequestWrapperTest extends \PHPUnit_Framework_TestCase
{
+ const VERSION = 'v0.1';
+
public function testSuccessfullySendsRequest()
{
$expectedBody = 'responseBody';
$response = new Response(200, [], $expectedBody);
+ $requestOptions = [
+ 'restOptions' => ['debug' => true],
+ 'requestTimeout' => 3.5
+ ];
$requestWrapper = new RequestWrapper([
'accessToken' => 'abc',
- 'httpHandler' => function ($request, $options = []) use ($response) {
+ 'httpHandler' => function ($request, $options = []) use ($response, $requestOptions) {
+ $this->assertEquals($requestOptions['restOptions']['debug'], $options['debug']);
+ $this->assertEquals($requestOptions['requestTimeout'], $options['timeout']);
return $response;
}
]);
$actualResponse = $requestWrapper->send(
- new Request('GET', 'http://www.test.com')
+ new Request('GET', 'http://www.test.com'),
+ $requestOptions
);
$this->assertEquals($expectedBody, (string) $actualResponse->getBody());
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testThrowsExceptionWhenRequestFails()
{
@@ -96,7 +106,7 @@ public function testCredentialsFetcher($wrapperConfig)
$requestWrapper = new RequestWrapper($wrapperConfig);
$this->assertInstanceOf(
- 'Google\Auth\FetchAuthTokenInterface',
+ FetchAuthTokenInterface::class,
$requestWrapper->getCredentialsFetcher()
);
}
@@ -111,7 +121,7 @@ public function testCredentialsFromKeyFileStreamCanBeReadMultipleTimes($wrapperC
$requestWrapper->getCredentialsFetcher();
$credentials = $requestWrapper->getCredentialsFetcher();
- $this->assertInstanceOf('Google\Auth\FetchAuthTokenInterface', $credentials);
+ $this->assertInstanceOf(FetchAuthTokenInterface::class, $credentials);
}
public function credentialsProvider()
@@ -125,10 +135,10 @@ public function credentialsProvider()
}
];
- $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json';
+ $keyFilePath = __DIR__ . '/../fixtures/json-key-fixture.json';
putenv("GOOGLE_APPLICATION_CREDENTIALS=$keyFilePath"); // for application default credentials
- $credentialsFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface');
+ $credentialsFetcher = $this->prophesize(FetchAuthTokenInterface::class);
$credentialsFetcher->fetchAuthToken(Argument::any())
->willReturn(['access_token' => 'abc']);
@@ -151,7 +161,7 @@ public function keyFileCredentialsProvider()
}
];
- $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json';
+ $keyFilePath = __DIR__ . '/../fixtures/json-key-fixture.json';
return [
[$config + ['keyFile' => json_decode(file_get_contents($keyFilePath), true)]], // keyFile
@@ -159,12 +169,15 @@ public function keyFileCredentialsProvider()
];
}
- public function testAddsUserAgentToRequest()
+ public function testAddsUserAgentAndXGoogApiClientToRequest()
{
$requestWrapper = new RequestWrapper([
+ 'componentVersion' => self::VERSION,
'httpHandler' => function ($request, $options = []) {
$userAgent = $request->getHeaderLine('User-Agent');
- $this->assertEquals('gcloud-php/' . ServiceBuilder::VERSION, $userAgent);
+ $this->assertEquals('gcloud-php/' . self::VERSION, $userAgent);
+ $xGoogApiClient = $request->getHeaderLine('x-goog-api-client');
+ $this->assertEquals('gl-php/' . phpversion() . ' gccl/' . self::VERSION, $xGoogApiClient);
return new Response(200);
},
'accessToken' => 'abc'
@@ -192,8 +205,30 @@ public function testAddsTokenToRequest()
);
}
+ public function testRequestUsesApiKeyInsteadOfAuthHeader()
+ {
+ $version = '1.0.0';
+ $requestWrapper = new RequestWrapper([
+ 'httpHandler' => function ($request, $options = []) use ($version) {
+ $authHeader = $request->getHeaderLine('Authorization');
+ $userAgent = $request->getHeaderLine('User-Agent');
+ $xGoogApiClient = $request->getHeaderLine('x-goog-api-client');
+ $this->assertEquals('gcloud-php/' . $version, $userAgent);
+ $this->assertEquals('gl-php/' . phpversion() . ' gccl/' . $version, $xGoogApiClient);
+ $this->assertEmpty($authHeader);
+ return new Response(200);
+ },
+ 'shouldSignRequest' => false,
+ 'componentVersion' => $version
+ ]);
+
+ $requestWrapper->send(
+ new Request('GET', 'http://www.example.com')
+ );
+ }
+
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testThrowsExceptionWhenFetchingCredentialsFails()
{
@@ -216,11 +251,12 @@ public function testExceptionMessageIsNotTruncatedWithGuzzle()
$requestWrapper = new RequestWrapper([
'httpHandler' => function ($request, $options = []) {
$msg = str_repeat('0', 121);
+ $jsonMsg = '{"msg":"' . $msg . '"}';
throw new RequestException(
- $msg,
+ $jsonMsg,
$request,
- new Response(400, [], $msg)
+ new Response(400, [], $jsonMsg)
);
}
]);
@@ -235,7 +271,7 @@ public function testExceptionMessageIsNotTruncatedWithGuzzle()
}
/**
- * @expectedException Google\Cloud\Exception\BadRequestException
+ * @expectedException Google\Cloud\Core\Exception\BadRequestException
*/
public function testThrowsBadRequestException()
{
@@ -251,7 +287,7 @@ public function testThrowsBadRequestException()
}
/**
- * @expectedException Google\Cloud\Exception\NotFoundException
+ * @expectedException Google\Cloud\Core\Exception\NotFoundException
*/
public function testThrowsNotFoundException()
{
@@ -267,7 +303,7 @@ public function testThrowsNotFoundException()
}
/**
- * @expectedException Google\Cloud\Exception\ConflictException
+ * @expectedException Google\Cloud\Core\Exception\ConflictException
*/
public function testThrowsConflictException()
{
@@ -283,7 +319,7 @@ public function testThrowsConflictException()
}
/**
- * @expectedException Google\Cloud\Exception\ServerException
+ * @expectedException Google\Cloud\Core\Exception\ServerException
*/
public function testThrowsServerException()
{
diff --git a/tests/unit/RestTraitTest.php b/tests/unit/Core/RestTraitTest.php
similarity index 79%
rename from tests/unit/RestTraitTest.php
rename to tests/unit/Core/RestTraitTest.php
index f323600b9898..b40db24868d7 100644
--- a/tests/unit/RestTraitTest.php
+++ b/tests/unit/Core/RestTraitTest.php
@@ -15,15 +15,17 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\RestTrait;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\RestTrait;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Prophecy\Argument;
/**
- * @group root
+ * @group core
*/
class RestTraitTest extends \PHPUnit_Framework_TestCase
{
@@ -34,8 +36,8 @@ class RestTraitTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
$this->implementation = $this->getObjectForTrait(RestTrait::class);
- $this->requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper');
- $this->requestBuilder = $this->prophesize('Google\Cloud\RequestBuilder');
+ $this->requestWrapper = $this->prophesize(RequestWrapper::class);
+ $this->requestBuilder = $this->prophesize(RequestBuilder::class);
$this->requestBuilder->build(Argument::cetera())
->willReturn(new Request('GET', '/someplace'));
}
@@ -55,17 +57,18 @@ public function testSendsRequest()
public function testSendsRequestWithOptions()
{
- $httpOptions = [
- 'httpOptions' => ['debug' => true],
- 'retries' => 5
+ $restOptions = [
+ 'restOptions' => ['debug' => true],
+ 'retries' => 5,
+ 'requestTimeout' => 3.5
];
$responseBody = '{"whatAWonderful": "response"}';
- $this->requestWrapper->send(Argument::any(), $httpOptions)
+ $this->requestWrapper->send(Argument::any(), $restOptions)
->willReturn(new Response(200, [], $responseBody));
$this->implementation->setRequestBuilder($this->requestBuilder->reveal());
$this->implementation->setRequestWrapper($this->requestWrapper->reveal());
- $actualResponse = $this->implementation->send('resource', 'method', $httpOptions);
+ $actualResponse = $this->implementation->send('resource', 'method', $restOptions);
$this->assertEquals(json_decode($responseBody, true), $actualResponse);
}
diff --git a/tests/unit/ServiceBuilderTest.php b/tests/unit/Core/ServiceBuilderTest.php
similarity index 97%
rename from tests/unit/ServiceBuilderTest.php
rename to tests/unit/Core/ServiceBuilderTest.php
index 5059d04e81d4..b91c87ecd066 100644
--- a/tests/unit/ServiceBuilderTest.php
+++ b/tests/unit/Core/ServiceBuilderTest.php
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
use Google\Cloud\ServiceBuilder;
use Google\Cloud\Translate\TranslateClient;
/**
- * @group root
+ * @group core
*/
class ServiceBuilderTest extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/Upload/MultipartUploaderTest.php b/tests/unit/Core/Upload/MultipartUploaderTest.php
similarity index 81%
rename from tests/unit/Upload/MultipartUploaderTest.php
rename to tests/unit/Core/Upload/MultipartUploaderTest.php
index ec184c0699f8..9882e4642d30 100644
--- a/tests/unit/Upload/MultipartUploaderTest.php
+++ b/tests/unit/Core/Upload/MultipartUploaderTest.php
@@ -15,28 +15,31 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Upload;
+namespace Google\Cloud\Tests\Unit\Core\Upload;
-use Google\Cloud\Upload\MultipartUploader;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\Upload\MultipartUploader;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Prophecy\Argument;
+use Psr\Http\Message\RequestInterface;
/**
+ * @group core
* @group upload
*/
class MultipartUploaderTest extends \PHPUnit_Framework_TestCase
{
public function testUploadsData()
{
- $requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper');
+ $requestWrapper = $this->prophesize(RequestWrapper::class);
$stream = Psr7\stream_for('abcd');
$successBody = '{"canI":"kickIt"}';
$response = new Response(200, [], $successBody);
$requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->willReturn($response);
diff --git a/tests/unit/Upload/ResumableUploaderTest.php b/tests/unit/Core/Upload/ResumableUploaderTest.php
similarity index 84%
rename from tests/unit/Upload/ResumableUploaderTest.php
rename to tests/unit/Core/Upload/ResumableUploaderTest.php
index f8123a5b95bb..b9f487935fd9 100644
--- a/tests/unit/Upload/ResumableUploaderTest.php
+++ b/tests/unit/Core/Upload/ResumableUploaderTest.php
@@ -15,15 +15,20 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Upload;
+namespace Google\Cloud\Tests\Unit\Core\Upload;
-use Google\Cloud\Upload\ResumableUploader;
+use Google\Cloud\Core\Exception\GoogleException;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\Upload\ResumableUploader;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Prophecy\Argument;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\StreamInterface;
/**
+ * @group core
* @group upload
*/
class ResumableUploaderTest extends \PHPUnit_Framework_TestCase
@@ -34,7 +39,7 @@ class ResumableUploaderTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
- $this->requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper');
+ $this->requestWrapper = $this->prophesize(RequestWrapper::class);
$this->stream = Psr7\stream_for('abcd');
$this->successBody = '{"canI":"kickIt"}';
}
@@ -44,7 +49,7 @@ public function testUploadsData()
$response = new Response(200, ['Location' => 'theResumeUri'], $this->successBody);
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->willReturn($response);
@@ -63,7 +68,7 @@ public function testGetResumeUri()
$response = new Response(200, ['Location' => $resumeUri]);
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->willReturn($response);
@@ -82,7 +87,7 @@ public function testResumesUpload()
$statusResponse = new Response(200, ['Range' => 'bytes 0-2']);
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->willReturn($response);
@@ -110,7 +115,7 @@ public function testResumeFinishedUpload()
$statusResponse = new Response(200, [], $this->successBody);
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->willReturn($statusResponse);
@@ -127,11 +132,11 @@ public function testResumeFinishedUpload()
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testThrowsExceptionWhenResumingNonSeekableStream()
{
- $stream = $this->prophesize('Psr\Http\Message\StreamInterface');
+ $stream = $this->prophesize(StreamInterface::class);
$stream->isSeekable()->willReturn(false);
$stream->getMetadata('uri')->willReturn('blah');
@@ -145,7 +150,7 @@ public function testThrowsExceptionWhenResumingNonSeekableStream()
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testThrowsExceptionWithFailedUpload()
{
@@ -159,7 +164,7 @@ public function testThrowsExceptionWithFailedUpload()
$this->requestWrapper->send(
Argument::which('getMethod', 'PUT'),
Argument::type('array')
- )->willThrow('Google\Cloud\Exception\GoogleException');
+ )->willThrow(GoogleException::class);
$uploader = new ResumableUploader(
$this->requestWrapper->reveal(),
diff --git a/tests/unit/Core/Upload/StreamableUploaderTest.php b/tests/unit/Core/Upload/StreamableUploaderTest.php
new file mode 100644
index 000000000000..c39ae80b14c7
--- /dev/null
+++ b/tests/unit/Core/Upload/StreamableUploaderTest.php
@@ -0,0 +1,150 @@
+requestWrapper = $this->prophesize(RequestWrapper::class);
+ $this->stream = new WriteStream(null, ['chunkSize' => 16]);
+ $this->successBody = '{"canI":"kickIt"}';
+ }
+
+ public function testStreamingWrites()
+ {
+ $resumeResponse = new Response(200, ['Location' => 'http://some-resume-uri.example.com'], $this->successBody);
+ $this->requestWrapper->send(
+ Argument::that(function($request){
+ return (string) $request->getUri() == 'http://www.example.com';
+ }),
+ Argument::type('array')
+ )->willReturn($resumeResponse);
+
+ $uploadResponse = new Response(200, [], $this->successBody);
+ $upload = $this->requestWrapper->send(
+ Argument::that(function($request){
+ return (string) $request->getUri() == 'http://some-resume-uri.example.com';
+ }),
+ Argument::type('array')
+ )->willReturn($uploadResponse);
+
+ $uploader = new StreamableUploader(
+ $this->requestWrapper->reveal(),
+ $this->stream,
+ 'http://www.example.com',
+ ['chunkSize' => 16]
+ );
+ $this->stream->setUploader($uploader);
+
+ // write some data smaller than the chunk size
+ $this->stream->write("0123456789");
+ $upload->shouldHaveBeenCalledTimes(0);
+
+ // write some more data that will put us over the chunk size.
+ $this->stream->write("more text");
+ $upload->shouldHaveBeenCalledTimes(1);
+
+ // finish the upload
+ $this->assertEquals(json_decode($this->successBody, true), $uploader->upload());
+ $upload->shouldHaveBeenCalledTimes(2);
+ }
+
+ public function testUploadsData()
+ {
+ $response = new Response(200, ['Location' => 'theResumeUri'], $this->successBody);
+
+ $this->requestWrapper->send(
+ Argument::type(RequestInterface::class),
+ Argument::type('array')
+ )->willReturn($response);
+
+ $uploader = new StreamableUploader(
+ $this->requestWrapper->reveal(),
+ $this->stream,
+ 'http://www.example.com'
+ );
+ $this->stream->setUploader($uploader);
+
+ $this->assertEquals(json_decode($this->successBody, true), $uploader->upload());
+ }
+
+ public function testGetResumeUri()
+ {
+ $resumeUri = 'theResumeUri';
+ $response = new Response(200, ['Location' => $resumeUri]);
+
+ $this->requestWrapper->send(
+ Argument::type(RequestInterface::class),
+ Argument::type('array')
+ )->willReturn($response);
+
+ $uploader = new StreamableUploader(
+ $this->requestWrapper->reveal(),
+ $this->stream,
+ 'http://www.example.com'
+ );
+ $this->stream->setUploader($uploader);
+
+ $this->assertEquals($resumeUri, $uploader->getResumeUri());
+ }
+
+ /**
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
+ */
+ public function testThrowsExceptionWithFailedUpload()
+ {
+ $resumeUriResponse = new Response(200, ['Location' => 'theResumeUri']);
+
+ $this->requestWrapper->send(
+ Argument::which('getMethod', 'POST'),
+ Argument::type('array')
+ )->willReturn($resumeUriResponse);
+
+ $this->requestWrapper->send(
+ Argument::which('getMethod', 'PUT'),
+ Argument::type('array')
+ )->willThrow(GoogleException::class);
+
+ $uploader = new StreamableUploader(
+ $this->requestWrapper->reveal(),
+ $this->stream,
+ 'http://www.example.com'
+ );
+
+ $uploader->upload();
+ }
+}
diff --git a/tests/unit/UriTraitTest.php b/tests/unit/Core/UriTraitTest.php
similarity index 95%
rename from tests/unit/UriTraitTest.php
rename to tests/unit/Core/UriTraitTest.php
index 1681ae411c6b..ab0761ccde7f 100644
--- a/tests/unit/UriTraitTest.php
+++ b/tests/unit/Core/UriTraitTest.php
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\UriTrait;
+use Google\Cloud\Core\UriTrait;
/**
- * @group root
+ * @group core
*/
class UriTraitTest extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/ValidateTraitTest.php b/tests/unit/Core/ValidateTraitTest.php
similarity index 95%
rename from tests/unit/ValidateTraitTest.php
rename to tests/unit/Core/ValidateTraitTest.php
index b40adbd19695..3dc730845227 100644
--- a/tests/unit/ValidateTraitTest.php
+++ b/tests/unit/Core/ValidateTraitTest.php
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit\Core;
-use Google\Cloud\ValidateTrait;
+use Google\Cloud\Core\ValidateTrait;
/**
- * @group root
+ * @group core
*/
class ValidateTraitTest extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/Datastore/BlobTest.php b/tests/unit/Datastore/BlobTest.php
index 469f0a06f08d..5a6971c04485 100644
--- a/tests/unit/Datastore/BlobTest.php
+++ b/tests/unit/Datastore/BlobTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\Blob;
use GuzzleHttp\Psr7;
diff --git a/tests/unit/Datastore/Connection/RestTest.php b/tests/unit/Datastore/Connection/RestTest.php
index 3483e1c6a79a..1fef3cb95a9c 100644
--- a/tests/unit/Datastore/Connection/RestTest.php
+++ b/tests/unit/Datastore/Connection/RestTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore\Connection;
+namespace Google\Cloud\Tests\Unit\Datastore\Connection;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\Datastore\Connection\Rest;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
+use Google\Cloud\Datastore\DatastoreClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
diff --git a/tests/unit/Datastore/DatastoreClientTest.php b/tests/unit/Datastore/DatastoreClientTest.php
index 5025f7bf08f4..14f9bfade3fe 100644
--- a/tests/unit/Datastore/DatastoreClientTest.php
+++ b/tests/unit/Datastore/DatastoreClientTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\Blob;
use Google\Cloud\Datastore\Connection\ConnectionInterface;
diff --git a/tests/unit/Datastore/DatastoreSessionHandlerTest.php b/tests/unit/Datastore/DatastoreSessionHandlerTest.php
index ff03edc716f7..55be63a759c3 100644
--- a/tests/unit/Datastore/DatastoreSessionHandlerTest.php
+++ b/tests/unit/Datastore/DatastoreSessionHandlerTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Exception;
diff --git a/tests/unit/Datastore/DatastoreTraitTest.php b/tests/unit/Datastore/DatastoreTraitTest.php
index 849232d12a1f..28c3c8531d91 100644
--- a/tests/unit/Datastore/DatastoreTraitTest.php
+++ b/tests/unit/Datastore/DatastoreTraitTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\DatastoreClient;
use Google\Cloud\Datastore\DatastoreTrait;
diff --git a/tests/unit/Datastore/EntityIteratorTest.php b/tests/unit/Datastore/EntityIteratorTest.php
new file mode 100644
index 000000000000..42063ee97d58
--- /dev/null
+++ b/tests/unit/Datastore/EntityIteratorTest.php
@@ -0,0 +1,40 @@
+prophesize(EntityPageIterator::class);
+ $pageIterator->moreResultsType()
+ ->willReturn($moreResultsType)
+ ->shouldBeCalledTimes(1);
+
+ $items = new EntityIterator($pageIterator->reveal());
+
+ $this->assertEquals($moreResultsType, $items->moreResultsType());
+ }
+}
diff --git a/tests/unit/Datastore/EntityMapperTest.php b/tests/unit/Datastore/EntityMapperTest.php
index 678e2eebde26..c0e2b1347dc5 100644
--- a/tests/unit/Datastore/EntityMapperTest.php
+++ b/tests/unit/Datastore/EntityMapperTest.php
@@ -15,14 +15,14 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\Blob;
use Google\Cloud\Datastore\Entity;
use Google\Cloud\Datastore\EntityMapper;
use Google\Cloud\Datastore\GeoPoint;
use Google\Cloud\Datastore\Key;
-use Google\Cloud\Int64;
+use Google\Cloud\Core\Int64;
/**
* @group datastore
diff --git a/tests/unit/Datastore/EntityPageIteratorTest.php b/tests/unit/Datastore/EntityPageIteratorTest.php
new file mode 100644
index 000000000000..b93bc57a1f90
--- /dev/null
+++ b/tests/unit/Datastore/EntityPageIteratorTest.php
@@ -0,0 +1,55 @@
+ 'call']
+ );
+
+ $pagesArray = iterator_to_array($pages);
+
+ $this->assertEquals(self::$moreResultsType, $pages->moreResultsType());
+ $this->assertEquals(self::$items, $pagesArray[0]);
+ }
+
+ public function theCall(array $options)
+ {
+ return [
+ 'batch' => [
+ 'moreResults' => self::$moreResultsType
+ ],
+ 'items' => self::$items
+ ];
+ }
+}
diff --git a/tests/unit/Datastore/EntityTest.php b/tests/unit/Datastore/EntityTest.php
index 27dd26ed9b8c..ca4e491df563 100644
--- a/tests/unit/Datastore/EntityTest.php
+++ b/tests/unit/Datastore/EntityTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\Entity;
use Google\Cloud\Datastore\EntityMapper;
diff --git a/tests/unit/Datastore/GeoPointTest.php b/tests/unit/Datastore/GeoPointTest.php
index 24d8fbc33a46..19e76f7f8906 100644
--- a/tests/unit/Datastore/GeoPointTest.php
+++ b/tests/unit/Datastore/GeoPointTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\GeoPoint;
diff --git a/tests/unit/Datastore/KeyTest.php b/tests/unit/Datastore/KeyTest.php
index 9520d6b3ebf5..b1705351707f 100644
--- a/tests/unit/Datastore/KeyTest.php
+++ b/tests/unit/Datastore/KeyTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\Key;
diff --git a/tests/unit/Datastore/OperationTest.php b/tests/unit/Datastore/OperationTest.php
index 39934e1b8c85..0df35468774d 100644
--- a/tests/unit/Datastore/OperationTest.php
+++ b/tests/unit/Datastore/OperationTest.php
@@ -15,10 +15,11 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\Connection\ConnectionInterface;
use Google\Cloud\Datastore\Entity;
+use Google\Cloud\Datastore\EntityIterator;
use Google\Cloud\Datastore\EntityMapper;
use Google\Cloud\Datastore\Key;
use Google\Cloud\Datastore\Operation;
@@ -443,7 +444,7 @@ public function testRunQuery()
$res = $this->operation->runQuery($q->reveal());
- $this->assertInstanceOf(\Generator::class, $res);
+ $this->assertInstanceOf(EntityIterator::class, $res);
$arr = iterator_to_array($res);
$this->assertEquals(count($arr), 2);
@@ -471,7 +472,7 @@ public function testRunQueryPaged()
$res = $this->operation->runQuery($q->reveal());
- $this->assertInstanceOf(\Generator::class, $res);
+ $this->assertInstanceOf(EntityIterator::class, $res);
$arr = iterator_to_array($res);
$this->assertEquals(count($arr), 3);
@@ -489,10 +490,11 @@ public function testRunQueryNoResults()
$q = $this->prophesize(QueryInterface::class);
$q->queryKey()->shouldBeCalled()->willReturn('query');
$q->queryObject()->shouldBeCalled()->willReturn([]);
+ $q->canPaginate()->shouldBeCalled()->willReturn(false);
$res = $this->operation->runQuery($q->reveal());
- $this->assertInstanceOf(\Generator::class, $res);
+ $this->assertInstanceOf(EntityIterator::class, $res);
$arr = iterator_to_array($res);
$this->assertEquals(count($arr), 0);
@@ -891,6 +893,11 @@ public function testInvalidBatchType()
class OperationStub extends Operation
{
+ // public function runQuery(QueryInterface $q, array $args = [])
+ // {
+ // echo 'test';
+ // exit;
+ // }
public function setConnection($connection)
{
$this->connection = $connection;
diff --git a/tests/unit/Datastore/Query/GqlQueryTest.php b/tests/unit/Datastore/Query/GqlQueryTest.php
index 5ad43de5d4d0..0e5ce9a6c3a6 100644
--- a/tests/unit/Datastore/Query/GqlQueryTest.php
+++ b/tests/unit/Datastore/Query/GqlQueryTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore\Query;
+namespace Google\Cloud\Tests\Unit\Datastore\Query;
use Google\Cloud\Datastore\EntityMapper;
use Google\Cloud\Datastore\Query\GqlQuery;
diff --git a/tests/unit/Datastore/Query/QueryTest.php b/tests/unit/Datastore/Query/QueryTest.php
index 5c6e31a34289..de0b01e1f5d8 100644
--- a/tests/unit/Datastore/Query/QueryTest.php
+++ b/tests/unit/Datastore/Query/QueryTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore\Query;
+namespace Google\Cloud\Tests\Unit\Datastore\Query;
use Google\Cloud\Datastore\EntityMapper;
use Google\Cloud\Datastore\Key;
diff --git a/tests/unit/Datastore/TransactionTest.php b/tests/unit/Datastore/TransactionTest.php
index d32902135482..dc7d8783a979 100644
--- a/tests/unit/Datastore/TransactionTest.php
+++ b/tests/unit/Datastore/TransactionTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Datastore;
+namespace Google\Cloud\Tests\Unit\Datastore;
use Google\Cloud\Datastore\Entity;
use Google\Cloud\Datastore\Key;
diff --git a/tests/unit/JsonFileTest.php b/tests/unit/JsonFileTest.php
index 9dea101b3c6f..ba747f5e8c2e 100644
--- a/tests/unit/JsonFileTest.php
+++ b/tests/unit/JsonFileTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests;
+namespace Google\Cloud\Tests\Unit;
use League\JsonGuard\Dereferencer;
use League\JsonGuard\Validator;
@@ -41,9 +41,36 @@ public function testComposer()
$validator = new Validator($json, $schema);
+ if ($validator->fails()) {
+ print_r($validator->errors());
+ }
+
$this->assertFalse($validator->fails());
}
+ public function testComponentComposer()
+ {
+ $files = glob(__DIR__ .'/../../src/*/composer.json');
+ foreach ($files as $file) {
+ $json = json_decode(file_get_contents($file));
+ $this->assertEquals(JSON_ERROR_NONE, json_last_error());
+
+ $deref = new Dereferencer();
+ $schema = $deref->dereference(json_decode(file_get_contents(sprintf(
+ self::SCHEMA_PATH,
+ __DIR__, 'composer.json.schema'
+ ))));
+
+ $validator = new Validator($json, $schema);
+
+ if ($validator->fails()) {
+ print_r($validator->errors());
+ }
+
+ $this->assertFalse($validator->fails());
+ }
+ }
+
public function testManifest()
{
$file = file_get_contents(__DIR__ .'/../../docs/manifest.json');
@@ -58,6 +85,10 @@ public function testManifest()
$validator = new Validator($json, $schema);
+ if ($validator->fails()) {
+ print_r($validator->errors());
+ }
+
$this->assertFalse($validator->fails());
}
@@ -75,6 +106,10 @@ public function testToc()
$validator = new Validator($json, $schema);
+ if ($validator->fails()) {
+ print_r($validator->errors());
+ }
+
$this->assertFalse($validator->fails());
}
}
diff --git a/tests/unit/Logging/Connection/GrpcTest.php b/tests/unit/Logging/Connection/GrpcTest.php
index be24988f73cc..317d0ff4e242 100644
--- a/tests/unit/Logging/Connection/GrpcTest.php
+++ b/tests/unit/Logging/Connection/GrpcTest.php
@@ -15,11 +15,11 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logging\Connection;
+namespace Google\Cloud\Tests\Unit\Logging\Connection;
use Google\Cloud\Logging\Connection\Grpc;
-use Google\Cloud\GrpcRequestWrapper;
-use Google\Cloud\PhpArray;
+use Google\Cloud\Core\GrpcRequestWrapper;
+use Google\Cloud\Core\PhpArray;
use Prophecy\Argument;
use google\logging\v2\LogEntry;
use google\logging\v2\LogMetric;
diff --git a/tests/unit/Logging/Connection/RestTest.php b/tests/unit/Logging/Connection/RestTest.php
index 5d6ca2fea252..a5f1eb718737 100644
--- a/tests/unit/Logging/Connection/RestTest.php
+++ b/tests/unit/Logging/Connection/RestTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logging\Connection;
+namespace Google\Cloud\Tests\Unit\Logging\Connection;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\Logging\Connection\Rest;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
+use Google\Cloud\Logging\LoggingClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
diff --git a/tests/unit/Logging/LoggerTest.php b/tests/unit/Logging/LoggerTest.php
index da402cb4da58..67fa64703e4f 100644
--- a/tests/unit/Logging/LoggerTest.php
+++ b/tests/unit/Logging/LoggerTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logging;
+namespace Google\Cloud\Tests\Unit\Logging;
use Google\Cloud\Logging\Logger;
use Google\Cloud\Logging\Connection\ConnectionInterface;
@@ -64,7 +64,6 @@ public function testGetsEntriesWithNoResults()
'pageSize' => 50
];
$this->connection->listEntries($options + [
- 'pageToken' => null,
'resourceNames' => ["projects/$this->projectId"],
'filter' => "logName = $this->formattedName"
])
@@ -121,7 +120,6 @@ public function testGetsEntriesWithAdditionalFilter()
{
$filter = 'textPayload = "hello world"';
$this->connection->listEntries([
- 'pageToken' => null,
'resourceNames' => ["projects/$this->projectId"],
'filter' => $filter . " AND logName = $this->formattedName"
])
diff --git a/tests/unit/Logging/LoggingClientTest.php b/tests/unit/Logging/LoggingClientTest.php
index 244847e0bcba..552cc0d93773 100644
--- a/tests/unit/Logging/LoggingClientTest.php
+++ b/tests/unit/Logging/LoggingClientTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logging;
+namespace Google\Cloud\Tests\Unit\Logging;
use Google\Cloud\Logging\Logger;
use Google\Cloud\Logging\LoggingClient;
@@ -208,7 +208,6 @@ public function testGetsEntriesWithNoResults()
{
$secondProjectId = 'secondProjectId';
$this->connection->listEntries([
- 'pageToken' => null,
'resourceNames' => [
'projects/' . $this->projectId,
'projects/' . $secondProjectId
diff --git a/tests/unit/Logging/MetricTest.php b/tests/unit/Logging/MetricTest.php
index 76db1e24dfa5..bc6380c54511 100644
--- a/tests/unit/Logging/MetricTest.php
+++ b/tests/unit/Logging/MetricTest.php
@@ -15,9 +15,9 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logging;
+namespace Google\Cloud\Tests\Unit\Logging;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Logging\Metric;
use Google\Cloud\Logging\Connection\ConnectionInterface;
use Prophecy\Argument;
diff --git a/tests/unit/Logging/PsrLoggerCompatabilityTest.php b/tests/unit/Logging/PsrLoggerCompatabilityTest.php
index e4dfceca7d22..f410bbc6318d 100644
--- a/tests/unit/Logging/PsrLoggerCompatabilityTest.php
+++ b/tests/unit/Logging/PsrLoggerCompatabilityTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logging;
+namespace Google\Cloud\Tests\Unit\Logging;
use Google\Cloud\Logging\Connection\ConnectionInterface;
use Google\Cloud\Logging\Logger;
diff --git a/tests/unit/Logging/PsrLoggerTest.php b/tests/unit/Logging/PsrLoggerTest.php
index 736854c8c0e3..55242fd9acf4 100644
--- a/tests/unit/Logging/PsrLoggerTest.php
+++ b/tests/unit/Logging/PsrLoggerTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logging;
+namespace Google\Cloud\Tests\Unit\Logging;
use Google\Cloud\Logging\Logger;
use Google\Cloud\Logging\PsrLogger;
diff --git a/tests/unit/Logging/SinkTest.php b/tests/unit/Logging/SinkTest.php
index c23c5f4ea750..3dc634b01476 100644
--- a/tests/unit/Logging/SinkTest.php
+++ b/tests/unit/Logging/SinkTest.php
@@ -15,9 +15,9 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Logging;
+namespace Google\Cloud\Tests\Unit\Logging;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Logging\Sink;
use Google\Cloud\Logging\Connection\ConnectionInterface;
use Prophecy\Argument;
diff --git a/tests/unit/NaturalLanguage/AnnotationTest.php b/tests/unit/NaturalLanguage/AnnotationTest.php
index a323d31dd15e..0cd93c1d18a2 100644
--- a/tests/unit/NaturalLanguage/AnnotationTest.php
+++ b/tests/unit/NaturalLanguage/AnnotationTest.php
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\NaturalLanguage;
+namespace Google\Cloud\Tests\Unit\NaturalLanguage;
use Google\Cloud\NaturalLanguage\Annotation;
/**
- * @group naturalLanguage
+ * @group naturallanguage
*/
class AnnotationTest extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/NaturalLanguage/Connection/RestTest.php b/tests/unit/NaturalLanguage/Connection/RestTest.php
index c5ccf0761ea6..f607a43b4769 100644
--- a/tests/unit/NaturalLanguage/Connection/RestTest.php
+++ b/tests/unit/NaturalLanguage/Connection/RestTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\NaturalLanguage\Connection;
+namespace Google\Cloud\Tests\Unit\NaturalLanguage\Connection;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\NaturalLanguage\Connection\Rest;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
+use Google\Cloud\NaturalLanguage\NaturalLanguageClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
@@ -28,7 +29,7 @@
use Rize\UriTemplate;
/**
- * @group naturalLanguage
+ * @group naturallanguage
*/
class RestTest extends \PHPUnit_Framework_TestCase
{
diff --git a/tests/unit/NaturalLanguage/NaturalLanguageClientTest.php b/tests/unit/NaturalLanguage/NaturalLanguageClientTest.php
index 19fbbef74645..26ccb0dd45b4 100644
--- a/tests/unit/NaturalLanguage/NaturalLanguageClientTest.php
+++ b/tests/unit/NaturalLanguage/NaturalLanguageClientTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\NaturalLanguage;
+namespace Google\Cloud\Tests\Unit\NaturalLanguage;
use Google\Cloud\NaturalLanguage\Annotation;
use Google\Cloud\NaturalLanguage\Connection\ConnectionInterface;
@@ -24,7 +24,7 @@
use Prophecy\Argument;
/**
- * @group naturalLanguage
+ * @group naturallanguage
*/
class NaturalLanguageClientTest extends \PHPUnit_Framework_TestCase
{
@@ -115,9 +115,23 @@ public function testAnnotateText($options, $expectedOptions)
public function analyzeDataProvider()
{
$objectMock = $this->prophesize(StorageObject::class);
- $objectMock->identity(Argument::any())->willReturn(['bucket' => 'bucket', 'object' => 'object']);
+ $gcsUri = 'gs://bucket/object';
+ $objectMock->gcsUri(Argument::any())->willReturn($gcsUri);
return [
+ [
+ [
+ 'content' => $gcsUri,
+ 'encodingType' => 'UTF16'
+ ],
+ [
+ 'document' => [
+ 'gcsContentUri' => $gcsUri,
+ 'type' => 'PLAIN_TEXT'
+ ],
+ 'encodingType' => 'UTF16'
+ ]
+ ],
[
[
'content' => 'My content.',
diff --git a/tests/unit/PubSub/Connection/GrpcTest.php b/tests/unit/PubSub/Connection/GrpcTest.php
index 970eabb3c6c0..61ff231740aa 100644
--- a/tests/unit/PubSub/Connection/GrpcTest.php
+++ b/tests/unit/PubSub/Connection/GrpcTest.php
@@ -15,10 +15,10 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub\Connection;
+namespace Google\Cloud\Tests\Unit\PubSub\Connection;
use Google\Cloud\PubSub\Connection\Grpc;
-use Google\Cloud\GrpcRequestWrapper;
+use Google\Cloud\Core\GrpcRequestWrapper;
use Prophecy\Argument;
use google\iam\v1\Binding;
use google\iam\v1\Policy;
diff --git a/tests/unit/PubSub/Connection/IamSubscriptionTest.php b/tests/unit/PubSub/Connection/IamSubscriptionTest.php
index 006a9f0682d0..cb7cfc6aa910 100644
--- a/tests/unit/PubSub/Connection/IamSubscriptionTest.php
+++ b/tests/unit/PubSub/Connection/IamSubscriptionTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub\Connection;
+namespace Google\Cloud\Tests\Unit\PubSub\Connection;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
use Google\Cloud\PubSub\Connection\IamSubscription;
diff --git a/tests/unit/PubSub/Connection/IamTopicTest.php b/tests/unit/PubSub/Connection/IamTopicTest.php
index 4fedb85eb855..1f751d5e18ad 100644
--- a/tests/unit/PubSub/Connection/IamTopicTest.php
+++ b/tests/unit/PubSub/Connection/IamTopicTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub\Connection;
+namespace Google\Cloud\Tests\Unit\PubSub\Connection;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
use Google\Cloud\PubSub\Connection\IamTopic;
diff --git a/tests/unit/PubSub/Connection/RestTest.php b/tests/unit/PubSub/Connection/RestTest.php
index bd13d614c524..6a4ad4d518f4 100644
--- a/tests/unit/PubSub/Connection/RestTest.php
+++ b/tests/unit/PubSub/Connection/RestTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub\Connection;
+namespace Google\Cloud\Tests\Unit\PubSub\Connection;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\PubSub\Connection\Rest;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
+use Google\Cloud\PubSub\PubSubClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
diff --git a/tests/unit/PubSub/IncomingMessageTraitTest.php b/tests/unit/PubSub/IncomingMessageTraitTest.php
index 19bfcd4f7454..e1a343b5109c 100644
--- a/tests/unit/PubSub/IncomingMessageTraitTest.php
+++ b/tests/unit/PubSub/IncomingMessageTraitTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub;
+namespace Google\Cloud\Tests\Unit\PubSub;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
use Google\Cloud\PubSub\IncomingMessageTrait;
@@ -47,7 +47,7 @@ public function testMessageFactory()
}
/**
- * @expectedException Google\Cloud\Exception\GoogleException
+ * @expectedException Google\Cloud\Core\Exception\GoogleException
*/
public function testInvalidMessage()
{
diff --git a/tests/unit/PubSub/MessageTest.php b/tests/unit/PubSub/MessageTest.php
index 0ab96a7f7667..770bbe7ba293 100644
--- a/tests/unit/PubSub/MessageTest.php
+++ b/tests/unit/PubSub/MessageTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub;
+namespace Google\Cloud\Tests\Unit\PubSub;
use Google\Cloud\PubSub\Message;
use Google\Cloud\PubSub\Subscription;
diff --git a/tests/unit/PubSub/PubSubClientTest.php b/tests/unit/PubSub/PubSubClientTest.php
index 52b339cff5e5..29258cd9c4b7 100644
--- a/tests/unit/PubSub/PubSubClientTest.php
+++ b/tests/unit/PubSub/PubSubClientTest.php
@@ -15,9 +15,9 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub;
+namespace Google\Cloud\Tests\Unit\PubSub;
-use Generator;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
use Google\Cloud\PubSub\Connection\Grpc;
use Google\Cloud\PubSub\Connection\Rest;
@@ -121,7 +121,7 @@ public function testTopics()
'foo' => 'bar'
]);
- $this->assertInstanceOf(Generator::class, $topics);
+ $this->assertInstanceOf(ItemIterator::class, $topics);
$arr = iterator_to_array($topics);
$this->assertInstanceOf(Topic::class, $arr[0]);
@@ -144,7 +144,9 @@ public function testTopicsPaged()
$this->connection->listTopics(Argument::that(function ($options) {
if ($options['foo'] !== 'bar') return false;
- if ($options['pageToken'] !== 'foo' && !is_null($options['pageToken'])) return false;
+ if (isset($options['pageToken']) && $options['pageToken'] !== 'foo') {
+ return false;
+ }
return true;
}))->willReturn([
@@ -230,7 +232,7 @@ public function testSubscriptions()
'foo' => 'bar'
]);
- $this->assertInstanceOf(Generator::class, $subscriptions);
+ $this->assertInstanceOf(ItemIterator::class, $subscriptions);
$arr = iterator_to_array($subscriptions);
$this->assertInstanceOf(Subscription::class, $arr[0]);
@@ -256,7 +258,9 @@ public function testSubscriptionsPaged()
$this->connection->listSubscriptions(Argument::that(function ($options) {
if ($options['foo'] !== 'bar') return false;
- if ($options['pageToken'] !== 'foo' && !is_null($options['pageToken'])) return false;
+ if (isset($options['pageToken']) && $options['pageToken'] !== 'foo') {
+ return false;
+ }
return true;
}))->willReturn([
diff --git a/tests/unit/PubSub/ResourceNameTraitTest.php b/tests/unit/PubSub/ResourceNameTraitTest.php
index 2cd87498b60c..306553be0379 100644
--- a/tests/unit/PubSub/ResourceNameTraitTest.php
+++ b/tests/unit/PubSub/ResourceNameTraitTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub;
+namespace Google\Cloud\Tests\Unit\PubSub;
use Google\Cloud\PubSub\ResourceNameTrait;
@@ -28,35 +28,35 @@ class ResourceNameTraitTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
- $this->trait = $this->getObjectForTrait(ResourceNameTrait::class);
+ $this->trait = new ResourceNameTraitStub;
}
public function testPluckProjectId()
{
- $res = $this->trait->pluckName(
+ $res = $this->trait->call('pluckName', [
'project',
'projects/foo'
- );
+ ]);
$this->assertEquals('foo', $res);
}
public function testPluckTopicName()
{
- $res = $this->trait->pluckName(
+ $res = $this->trait->call('pluckName', [
'topic',
'projects/foo/topics/bar'
- );
+ ]);
$this->assertEquals('bar', $res);
}
public function testPluckSubscriptionName()
{
- $res = $this->trait->pluckName(
+ $res = $this->trait->call('pluckName', [
'subscription',
'projects/foo/subscriptions/bar'
- );
+ ]);
$this->assertEquals('bar', $res);
}
@@ -66,26 +66,26 @@ public function testPluckSubscriptionName()
*/
public function testPluckNameInvalidFormat()
{
- $this->trait->pluckName('lame', 'bar');
+ $this->trait->call('pluckName', ['lame', 'bar']);
}
public function testFormatProjectId()
{
- $res = $this->trait->formatName('project', 'foo');
+ $res = $this->trait->call('formatName', ['project', 'foo']);
$this->assertEquals('projects/foo', $res);
}
public function testFormatTopicName()
{
- $res = $this->trait->formatName('topic', 'foo', 'my-project');
+ $res = $this->trait->call('formatName', ['topic', 'foo', 'my-project']);
$this->assertEquals('projects/my-project/topics/foo', $res);
}
public function testFormatSubscriptionName()
{
- $res = $this->trait->formatName('subscription', 'foo', 'my-project');
+ $res = $this->trait->call('formatName', ['subscription', 'foo', 'my-project']);
$this->assertEquals('projects/my-project/subscriptions/foo', $res);
}
@@ -95,46 +95,46 @@ public function testFormatSubscriptionName()
*/
public function testFormatNameInvalidType()
{
- $this->trait->formatName('lame', ['foo']);
+ $this->trait->call('formatName', ['lame', 'foo']);
}
public function testIsFullyQualifiedProjectId()
{
- $this->assertTrue($this->trait->isFullyQualifiedName(
+ $this->assertTrue($this->trait->call('isFullyQualifiedName', [
'project',
'projects/foo'
- ));
+ ]));
- $this->assertFalse($this->trait->isFullyQualifiedName(
+ $this->assertFalse($this->trait->call('isFullyQualifiedName', [
'project',
'foo'
- ));
+ ]));
}
public function testIsFullyQualifiedTopicName()
{
- $this->assertTrue($this->trait->isFullyQualifiedName(
+ $this->assertTrue($this->trait->call('isFullyQualifiedName', [
'topic',
'projects/foo/topics/bar'
- ));
+ ]));
- $this->assertFalse($this->trait->isFullyQualifiedName(
+ $this->assertFalse($this->trait->call('isFullyQualifiedName', [
'topic',
'foo'
- ));
+ ]));
}
public function testIsFullyQualifiedSubscriptionName()
{
- $this->assertTrue($this->trait->isFullyQualifiedName(
+ $this->assertTrue($this->trait->call('isFullyQualifiedName', [
'subscription',
'projects/foo/subscriptions/bar'
- ));
+ ]));
- $this->assertFalse($this->trait->isFullyQualifiedName(
+ $this->assertFalse($this->trait->call('isFullyQualifiedName', [
'subscription',
'foo'
- ));
+ ]));
}
/**
@@ -142,6 +142,16 @@ public function testIsFullyQualifiedSubscriptionName()
*/
public function testIsFullyQualifiedNameInvalidType()
{
- $this->trait->isFullyQualifiedName('lame', 'foo');
+ $this->trait->call('isFullyQualifiedName', ['lame', 'foo']);
+ }
+}
+
+class ResourceNameTraitStub
+{
+ use ResourceNameTrait;
+
+ public function call($method, array $args)
+ {
+ return call_user_func_array([$this, $method], $args);
}
}
diff --git a/tests/unit/PubSub/SubscriptionTest.php b/tests/unit/PubSub/SubscriptionTest.php
index 3dda439663a3..eceac1c3ad50 100644
--- a/tests/unit/PubSub/SubscriptionTest.php
+++ b/tests/unit/PubSub/SubscriptionTest.php
@@ -15,11 +15,11 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub;
+namespace Google\Cloud\Tests\Unit\PubSub;
-use Generator;
-use Google\Cloud\Exception\NotFoundException;
-use Google\Cloud\Iam\Iam;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Core\Iam\Iam;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
use Google\Cloud\PubSub\Message;
use Google\Cloud\PubSub\Subscription;
@@ -199,11 +199,9 @@ public function testPull()
'foo' => 'bar'
]);
- $this->assertInstanceOf(Generator::class, $result);
-
- $arr = iterator_to_array($result);
- $this->assertInstanceOf(Message::class, $arr[0]);
- $this->assertInstanceOf(Message::class, $arr[1]);
+ $this->assertContainsOnlyInstancesOf(Message::class, $result);
+ $this->assertInstanceOf(Message::class, $result[0]);
+ $this->assertInstanceOf(Message::class, $result[1]);
}
public function testPullWithCustomArgs()
@@ -235,56 +233,9 @@ public function testPullWithCustomArgs()
'maxMessages' => 2
]);
- $this->assertInstanceOf(Generator::class, $result);
-
- $arr = iterator_to_array($result);
- $this->assertInstanceOf(Message::class, $arr[0]);
- $this->assertInstanceOf(Message::class, $arr[1]);
- }
-
- public function testPullPaged()
- {
- $messages = [
- 'receivedMessages' => [
- [
- 'message' => []
- ], [
- 'message' => []
- ]
- ],
- 'nextPageToken' => 'foo'
- ];
-
- $this->connection->pull(Argument::that(function ($args) {
- if ($args['foo'] !== 'bar') return false;
- if ($args['returnImmediately'] !== true) return false;
- if ($args['maxMessages'] !== 2) return false;
- if (!in_array($args['pageToken'], [null, 'foo'])) return false;
-
- return true;
- }))->willReturn($messages)
- ->shouldBeCalledTimes(3);
-
- $this->subscription->setConnection($this->connection->reveal());
-
- $result = $this->subscription->pull([
- 'foo' => 'bar',
- 'returnImmediately' => true,
- 'maxMessages' => 2
- ]);
-
- $this->assertInstanceOf(Generator::class, $result);
-
- // enumerate the iterator and kill after it loops twice.
- $arr = [];
- $i = 0;
- foreach ($result as $message) {
- $i++;
- $arr[] = $message;
- if ($i == 6) break;
- }
-
- $this->assertEquals(6, count($arr));
+ $this->assertContainsOnlyInstancesOf(Message::class, $result);
+ $this->assertInstanceOf(Message::class, $result[0]);
+ $this->assertInstanceOf(Message::class, $result[1]);
}
public function testAcknowledge()
diff --git a/tests/unit/PubSub/TopicTest.php b/tests/unit/PubSub/TopicTest.php
index eb45676a0963..eb4da74063b4 100644
--- a/tests/unit/PubSub/TopicTest.php
+++ b/tests/unit/PubSub/TopicTest.php
@@ -15,11 +15,11 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\PubSub;
+namespace Google\Cloud\Tests\Unit\PubSub;
-use Generator;
-use Google\Cloud\Exception\NotFoundException;
-use Google\Cloud\Iam\Iam;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Core\Iam\Iam;
+use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\PubSub\Connection\ConnectionInterface;
use Google\Cloud\PubSub\Subscription;
use Google\Cloud\PubSub\Topic;
@@ -282,7 +282,7 @@ public function testSubscriptions()
'foo' => 'bar'
]);
- $this->assertInstanceOf(Generator::class, $subscriptions);
+ $this->assertInstanceOf(ItemIterator::class, $subscriptions);
$arr = iterator_to_array($subscriptions);
$this->assertInstanceOf(Subscription::class, $arr[0]);
@@ -301,7 +301,9 @@ public function testSubscriptionsPaged()
$this->connection->listSubscriptionsByTopic(Argument::that(function ($options) {
if ($options['foo'] !== 'bar') return false;
- if ($options['pageToken'] !== 'foo' && !is_null($options['pageToken'])) return false;
+ if (isset($options['pageToken']) && $options['pageToken'] !== 'foo') {
+ return false;
+ }
return true;
}))->willReturn([
diff --git a/tests/unit/Spanner/BytesTest.php b/tests/unit/Spanner/BytesTest.php
new file mode 100644
index 000000000000..d224738f96e2
--- /dev/null
+++ b/tests/unit/Spanner/BytesTest.php
@@ -0,0 +1,52 @@
+content);
+ $this->assertEquals($this->content, $bytes->get());
+ }
+
+ public function testFormatAsString()
+ {
+ $bytes = new Bytes($this->content);
+ $this->assertEquals(base64_encode($this->content), $bytes->formatAsString());
+ }
+
+ public function testCast()
+ {
+ $bytes = new Bytes($this->content);
+ $this->assertEquals(base64_encode($this->content), (string) $bytes);
+ }
+
+ public function testType()
+ {
+ $bytes = new Bytes($this->content);
+ $this->assertTrue(is_integer($bytes->type()));
+ }
+}
diff --git a/tests/unit/Spanner/ConfigurationTest.php b/tests/unit/Spanner/ConfigurationTest.php
new file mode 100644
index 000000000000..9cdceb8c336d
--- /dev/null
+++ b/tests/unit/Spanner/ConfigurationTest.php
@@ -0,0 +1,116 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->configuration = \Google\Cloud\Dev\stub(Configuration::class, [
+ $this->connection->reveal(),
+ self::PROJECT_ID,
+ self::NAME
+ ]);
+ }
+
+ public function testName()
+ {
+ $this->assertEquals(self::NAME, $this->configuration->name());
+ }
+
+ public function testInfo()
+ {
+ $this->connection->getConfig(Argument::any())->shouldNotBeCalled();
+ $this->configuration->___setProperty('connection', $this->connection->reveal());
+
+ $info = ['foo' => 'bar'];
+ $config = \Google\Cloud\Dev\stub(Configuration::class, [
+ $this->connection->reveal(),
+ self::PROJECT_ID,
+ self::NAME,
+ $info
+ ]);
+
+ $this->assertEquals($info, $config->info());
+ }
+
+ public function testInfoWithReload()
+ {
+ $info = ['foo' => 'bar'];
+
+ $this->connection->getConfig([
+ 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME),
+ 'projectId' => self::PROJECT_ID
+ ])->shouldBeCalled()->willReturn($info);
+
+ $this->configuration->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertEquals($info, $this->configuration->info());
+ }
+
+ public function testExists()
+ {
+ $this->connection->getConfig(Argument::any())->willReturn([]);
+ $this->configuration->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertTrue($this->configuration->exists());
+ }
+
+ public function testExistsDoesntExist()
+ {
+ $this->connection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404));
+ $this->configuration->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertFalse($this->configuration->exists());
+ }
+
+ public function testReload()
+ {
+ $info = ['foo' => 'bar'];
+
+ $this->connection->getConfig([
+ 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME),
+ 'projectId' => self::PROJECT_ID
+ ])->shouldBeCalledTimes(1)->willReturn($info);
+
+ $this->configuration->___setProperty('connection', $this->connection->reveal());
+
+ $info = $this->configuration->reload();
+
+ $info2 = $this->configuration->info();
+
+ $this->assertEquals($info, $info2);
+ }
+}
diff --git a/tests/unit/Spanner/Connection/IamDatabaseTest.php b/tests/unit/Spanner/Connection/IamDatabaseTest.php
new file mode 100644
index 000000000000..fafbb2f93eaf
--- /dev/null
+++ b/tests/unit/Spanner/Connection/IamDatabaseTest.php
@@ -0,0 +1,66 @@
+connection = $this->prophesize(ConnectionInterface::class);
+
+ $this->iam = \Google\Cloud\Dev\stub(IamDatabase::class, [$this->connection->reveal()]);
+ }
+
+ /**
+ * @dataProvider methodProvider
+ */
+ public function testMethods($methodName, $proxyName, $args)
+ {
+ $this->connection->$proxyName($args)
+ ->shouldBeCalled()
+ ->willReturn($args);
+
+ $this->iam->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->iam->$methodName($args);
+ $this->assertEquals($args, $res);
+ }
+
+ public function methodProvider()
+ {
+ $args = ['foo' => 'bar'];
+
+ return [
+ ['getPolicy', 'getDatabaseIamPolicy', $args],
+ ['setPolicy', 'setDatabaseIamPolicy', $args],
+ ['testPermissions', 'testDatabaseIamPermissions', $args]
+ ];
+ }
+}
diff --git a/tests/unit/Spanner/Connection/IamInstanceTest.php b/tests/unit/Spanner/Connection/IamInstanceTest.php
new file mode 100644
index 000000000000..426a4a125245
--- /dev/null
+++ b/tests/unit/Spanner/Connection/IamInstanceTest.php
@@ -0,0 +1,66 @@
+connection = $this->prophesize(ConnectionInterface::class);
+
+ $this->iam = \Google\Cloud\Dev\stub(IamInstance::class, [$this->connection->reveal()]);
+ }
+
+ /**
+ * @dataProvider methodProvider
+ */
+ public function testMethods($methodName, $proxyName, $args)
+ {
+ $this->connection->$proxyName($args)
+ ->shouldBeCalled()
+ ->willReturn($args);
+
+ $this->iam->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->iam->$methodName($args);
+ $this->assertEquals($args, $res);
+ }
+
+ public function methodProvider()
+ {
+ $args = ['foo' => 'bar'];
+
+ return [
+ ['getPolicy', 'getInstanceIamPolicy', $args],
+ ['setPolicy', 'setInstanceIamPolicy', $args],
+ ['testPermissions', 'testInstanceIamPermissions', $args]
+ ];
+ }
+}
diff --git a/tests/unit/Spanner/Connection/LongRunningConnectionTest.php b/tests/unit/Spanner/Connection/LongRunningConnectionTest.php
new file mode 100644
index 000000000000..f1ce2489d281
--- /dev/null
+++ b/tests/unit/Spanner/Connection/LongRunningConnectionTest.php
@@ -0,0 +1,66 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->lro = \Google\Cloud\Dev\stub(LongRunningConnection::class, [
+ $this->connection->reveal()
+ ]);
+ }
+
+ /**
+ * @dataProvider methodProvider
+ */
+ public function testMethods($methodName, $proxyName, $args)
+ {
+ $this->connection->$proxyName($args)
+ ->shouldBeCalled()
+ ->willReturn($args);
+
+ $this->lro->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->lro->$methodName($args);
+ $this->assertEquals($args, $res);
+ }
+
+ public function methodProvider()
+ {
+ $args = ['foo' => 'bar'];
+
+ return [
+ ['get', 'getOperation', $args],
+ ['cancel', 'cancelOperation', $args],
+ ['delete', 'deleteOperation', $args],
+ ['operations', 'listOperations', $args]
+ ];
+ }
+}
diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php
new file mode 100644
index 000000000000..5216a69fc6f0
--- /dev/null
+++ b/tests/unit/Spanner/DatabaseTest.php
@@ -0,0 +1,620 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->instance = $this->prophesize(Instance::class);
+ $this->sessionPool = $this->prophesize(SessionPoolInterface::class);
+ $this->lro = $this->prophesize(LongRunningConnectionInterface::class);
+ $this->lroCallables = [];
+
+ $this->sessionPool->session(self::INSTANCE, self::DATABASE, Argument::any())
+ ->willReturn(new Session(
+ $this->connection->reveal(),
+ self::PROJECT,
+ self::INSTANCE,
+ self::DATABASE,
+ self::SESSION
+ ));
+
+ $this->instance->name()->willReturn(self::INSTANCE);
+
+ $args = [
+ $this->connection->reveal(),
+ $this->instance->reveal(),
+ $this->sessionPool->reveal(),
+ $this->lro->reveal(),
+ $this->lroCallables,
+ self::PROJECT,
+ self::DATABASE,
+ ];
+
+ $props = [
+ 'connection', 'operation'
+ ];
+
+ $this->database = \Google\Cloud\Dev\stub(Database::class, $args, $props);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testExists()
+ {
+ $this->connection->getDatabaseDDL(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([]);
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertTrue($this->database->exists());
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testExistsNotFound()
+ {
+ $this->connection->getDatabaseDDL(Argument::any())
+ ->shouldBeCalled()
+ ->willThrow(new NotFoundException('', 404));
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertFalse($this->database->exists());
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testUpdateDdl()
+ {
+ $statement = 'foo';
+ $this->connection->updateDatabase([
+ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE),
+ 'statements' => [$statement]
+ ]);
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $this->database->updateDdl($statement);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testUpdateDdlBatch()
+ {
+ $statements = ['foo', 'bar'];
+ $this->connection->updateDatabase([
+ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE),
+ 'statements' => $statements
+ ]);
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $this->database->updateDdl($statements);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testUpdateWithSingleStatement()
+ {
+ $statement = 'foo';
+ $this->connection->updateDatabase([
+ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE),
+ 'statements' => ['foo']
+ ])->shouldBeCalled()->willReturn(['name' => 'operations/foo']);
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->database->updateDdl($statement);
+ $this->assertInstanceOf(LongRunningOperation::class, $res);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testDrop()
+ {
+ $this->connection->dropDatabase([
+ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE)
+ ])->shouldBeCalled();
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $this->database->drop();
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testDdl()
+ {
+ $ddl = ['create table users', 'create table posts'];
+ $this->connection->getDatabaseDDL([
+ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE)
+ ])->willReturn(['statements' => $ddl]);
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertEquals($ddl, $this->database->ddl());
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testDdlNoResult()
+ {
+ $this->connection->getDatabaseDDL([
+ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE)
+ ])->willReturn([]);
+
+ $this->database->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertEquals([], $this->database->ddl());
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testIam()
+ {
+ $this->assertInstanceOf(Iam::class, $this->database->iam());
+ }
+
+ public function testSnapshot()
+ {
+ $this->connection->beginTransaction(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['id' => self::TRANSACTION]);
+
+ $this->refreshOperation();
+
+ $res = $this->database->snapshot();
+ $this->assertInstanceOf(Snapshot::class, $res);
+ }
+
+ /**
+ * @expectedException BadMethodCallException
+ */
+ public function testSnapshotMinReadTimestamp()
+ {
+ $this->database->snapshot(['minReadTimestamp' => 'foo']);
+ }
+
+ /**
+ * @expectedException BadMethodCallException
+ */
+ public function testSnapshotMaxStaleness()
+ {
+ $this->database->snapshot(['maxStaleness' => 'foo']);
+ }
+
+ public function testRunTransaction()
+ {
+ $this->connection->beginTransaction(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['id' => self::TRANSACTION]);
+
+ $this->refreshOperation();
+
+ $hasTransaction = false;
+
+ $this->database->runTransaction(function (Transaction $t) use (&$hasTransaction) {
+ $hasTransaction = true;
+ });
+
+ $this->assertTrue($hasTransaction);
+ }
+
+ public function testRunTransactionRetry()
+ {
+ $abort = new AbortedException('foo', 409, null, [
+ [
+ 'retryDelay' => [
+ 'seconds' => 1,
+ 'nanos' => 0
+ ]
+ ]
+ ]);
+
+ $this->connection->beginTransaction(Argument::any())
+ ->shouldBeCalledTimes(3)
+ ->willReturn(['id' => self::TRANSACTION]);
+
+ $it = 0;
+ $this->connection->commit(Argument::any())
+ ->shouldBeCalledTimes(3)
+ ->will(function() use (&$it, $abort) {
+ $it++;
+ if ($it <= 2) {
+ throw $abort;
+ }
+
+ return ['commitTimestamp' => TransactionTest::TIMESTAMP];
+ });
+
+ $this->refreshOperation();
+
+ $this->database->runTransaction(function($t){$t->commit();});
+ }
+
+ /**
+ * @expectedException Google\Cloud\Core\Exception\AbortedException
+ */
+ public function testRunTransactionAborted()
+ {
+ $abort = new AbortedException('foo', 409, null, [
+ [
+ 'retryDelay' => [
+ 'seconds' => 1,
+ 'nanos' => 0
+ ]
+ ]
+ ]);
+
+ $this->connection->beginTransaction(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['id' => self::TRANSACTION]);
+
+ $it = 0;
+ $this->connection->commit(Argument::any())
+ ->shouldBeCalled()
+ ->will(function() use (&$it, $abort) {
+ $it++;
+ if ($it <= 8) {
+ throw $abort;
+ }
+
+ return ['commitTimestamp' => TransactionTest::TIMESTAMP];
+ });
+
+ $this->refreshOperation();
+
+ $this->database->runTransaction(function($t){$t->commit();});
+ }
+
+ public function testTransaction()
+ {
+ $this->connection->beginTransaction(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['id' => self::TRANSACTION]);
+
+ $this->refreshOperation();
+
+ $t = $this->database->transaction();
+ $this->assertInstanceOf(Transaction::class, $t);
+ }
+
+ public function testInsert()
+ {
+ $table = 'foo';
+ $row = ['col' => 'val'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $row) {
+ if ($arg['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) return false;
+ if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) return false;
+ if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->insert($table, $row);
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testInsertBatch()
+ {
+ $table = 'foo';
+ $row = ['col' => 'val'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $row) {
+ if ($arg['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) return false;
+ if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) return false;
+ if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->insertBatch($table, [$row]);
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testUpdate()
+ {
+ $table = 'foo';
+ $row = ['col' => 'val'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $row) {
+ if ($arg['mutations'][0][Operation::OP_UPDATE]['table'] !== $table) return false;
+ if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) return false;
+ if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->update($table, $row);
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testUpdateBatch()
+ {
+ $table = 'foo';
+ $row = ['col' => 'val'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $row) {
+ if ($arg['mutations'][0][Operation::OP_UPDATE]['table'] !== $table) return false;
+ if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) return false;
+ if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->updateBatch($table, [$row]);
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testInsertOrUpdate()
+ {
+ $table = 'foo';
+ $row = ['col' => 'val'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $row) {
+ if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['table'] !== $table) return false;
+ if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) return false;
+ if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->insertOrUpdate($table, $row);
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testInsertOrUpdateBatch()
+ {
+ $table = 'foo';
+ $row = ['col' => 'val'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $row) {
+ if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['table'] !== $table) return false;
+ if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) return false;
+ if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->insertOrUpdateBatch($table, [$row]);
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testReplace()
+ {
+ $table = 'foo';
+ $row = ['col' => 'val'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $row) {
+ if ($arg['mutations'][0][Operation::OP_REPLACE]['table'] !== $table) return false;
+ if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) return false;
+ if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->replace($table, $row);
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testReplaceBatch()
+ {
+ $table = 'foo';
+ $row = ['col' => 'val'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $row) {
+ if ($arg['mutations'][0][Operation::OP_REPLACE]['table'] !== $table) return false;
+ if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) return false;
+ if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->replaceBatch($table, [$row]);
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testDelete()
+ {
+ $table = 'foo';
+ $keys = [10, 'bar'];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($table, $keys) {
+ if ($arg['mutations'][0][Operation::OP_DELETE]['table'] !== $table) return false;
+ if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][0] !== (string) $keys[0]) return false;
+ if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][1] !== $keys[1]) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->commitResponse());
+
+ $this->refreshOperation();
+
+ $res = $this->database->delete($table, new KeySet(['keys' => $keys]));
+ $this->assertInstanceOf(Timestamp::class, $res);
+ $this->assertTimestampIsCorrect($res);
+ }
+
+ public function testExecute()
+ {
+ $sql = 'SELECT * FROM Table';
+
+ $this->connection->executeSql(Argument::that(function ($arg) use ($sql) {
+ if ($arg['sql'] !== $sql) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn([
+ 'metadata' => [
+ 'rowType' => [
+ 'fields' => [
+ [
+ 'name' => 'ID',
+ 'type' => [
+ 'code' => ValueMapper::TYPE_INT64
+ ]
+ ]
+ ]
+ ]
+ ],
+ 'rows' => [
+ [
+ '10'
+ ]
+ ]
+ ]);
+
+ $this->refreshOperation();
+
+ $res = $this->database->execute($sql);
+ $this->assertInstanceOf(Result::class, $res);
+ $this->assertEquals(10, $res->rows()[0]['ID']);
+ }
+
+ public function testRead()
+ {
+ $table = 'Table';
+ $opts = ['foo' => 'bar'];
+
+ $this->connection->read(Argument::that(function ($arg) use ($table, $opts) {
+ if ($arg['table'] !== $table) return false;
+ if ($arg['keySet']['all'] !== true) return false;
+ if ($arg['columns'] !== ['ID']) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn([
+ 'metadata' => [
+ 'rowType' => [
+ 'fields' => [
+ [
+ 'name' => 'ID',
+ 'type' => [
+ 'code' => ValueMapper::TYPE_INT64
+ ]
+ ]
+ ]
+ ]
+ ],
+ 'rows' => [
+ [
+ '10'
+ ]
+ ]
+ ]);
+
+ $this->refreshOperation();
+
+ $res = $this->database->read($table, new KeySet(['all' => true]), ['ID']);
+ $this->assertInstanceOf(Result::class, $res);
+ $this->assertEquals(10, $res->rows()[0]['ID']);
+ }
+
+ // *******
+ // Helpers
+
+ private function refreshOperation()
+ {
+ $operation = new Operation($this->connection->reveal(), false);
+ $this->database->___setProperty('operation', $operation);
+ }
+
+ private function commitResponse()
+ {
+ return ['commitTimestamp' => '2017-01-09T18:05:22.534799Z'];
+ }
+
+ private function assertTimestampIsCorrect($res)
+ {
+ $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']);
+
+ $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z'));
+ }
+}
diff --git a/tests/unit/Spanner/DateTest.php b/tests/unit/Spanner/DateTest.php
new file mode 100644
index 000000000000..1c25c6bc3245
--- /dev/null
+++ b/tests/unit/Spanner/DateTest.php
@@ -0,0 +1,55 @@
+dt = new \DateTime('1989-10-11');
+ $this->date = new Date($this->dt);
+ }
+
+ public function testGet()
+ {
+ $this->assertEquals($this->dt, $this->date->get());
+ }
+
+ public function testFormatAsString()
+ {
+ $this->assertEquals($this->dt->format(Date::FORMAT), $this->date->formatAsString());
+ }
+
+ public function testCast()
+ {
+ $this->assertEquals($this->dt->format(Date::FORMAT), (string)$this->date);
+ }
+
+ public function testType()
+ {
+ $this->assertTrue(is_integer($this->date->type()));
+ }
+}
diff --git a/tests/unit/Spanner/DurationTest.php b/tests/unit/Spanner/DurationTest.php
new file mode 100644
index 000000000000..37d2575e42f7
--- /dev/null
+++ b/tests/unit/Spanner/DurationTest.php
@@ -0,0 +1,65 @@
+duration = new Duration(self::SECONDS, self::NANOS);
+ }
+
+ public function testGet()
+ {
+ $this->assertEquals([
+ 'seconds' => self::SECONDS,
+ 'nanos' => self::NANOS
+ ], $this->duration->get());
+ }
+
+ public function testType()
+ {
+ $this->assertEquals(Duration::TYPE, $this->duration->type());
+ }
+
+ public function testFormatAsString()
+ {
+ $this->assertEquals(
+ json_encode($this->duration->get()),
+ $this->duration->formatAsString()
+ );
+ }
+
+ public function testTostring()
+ {
+ $this->assertEquals(
+ json_encode($this->duration->get()),
+ (string)$this->duration
+ );
+ }
+}
diff --git a/tests/unit/Spanner/InstanceTest.php b/tests/unit/Spanner/InstanceTest.php
new file mode 100644
index 000000000000..5612ce712733
--- /dev/null
+++ b/tests/unit/Spanner/InstanceTest.php
@@ -0,0 +1,318 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->instance = \Google\Cloud\Dev\stub(Instance::class, [
+ $this->connection->reveal(),
+ $this->prophesize(SessionPoolInterface::class)->reveal(),
+ $this->prophesize(LongRunningConnectionInterface::class)->reveal(),
+ [],
+ self::PROJECT_ID,
+ self::NAME
+ ], [
+ 'info',
+ 'connection'
+ ]);
+ }
+
+ public function testName()
+ {
+ $this->assertEquals(self::NAME, $this->instance->name());
+ }
+
+ public function testInfo()
+ {
+ $this->connection->getInstance()->shouldNotBeCalled();
+
+ $this->instance->___setProperty('info', ['foo' => 'bar']);
+ $this->assertEquals('bar', $this->instance->info()['foo']);
+ }
+
+ public function testInfoWithReload()
+ {
+ $instance = $this->getDefaultInstance();
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn($instance);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $info = $this->instance->info();
+ $this->assertEquals('Instance Name', $info['displayName']);
+
+ $this->assertEquals($info, $this->instance->info());
+ }
+
+ public function testExists()
+ {
+ $this->connection->getInstance(Argument::any())->shouldBeCalled()->willReturn([]);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertTrue($this->instance->exists());
+ }
+
+ public function testExistsNotFound()
+ {
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalled()
+ ->willThrow(new NotFoundException('foo', 404));
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertFalse($this->instance->exists());
+ }
+
+ public function testReload()
+ {
+ $instance = $this->getDefaultInstance();
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn($instance);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $info = $this->instance->reload();
+
+ $this->assertEquals('Instance Name', $info['displayName']);
+ }
+
+ public function testState()
+ {
+ $instance = $this->getDefaultInstance();
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn($instance);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertEquals(Instance::STATE_READY, $this->instance->state());
+ }
+
+ public function testStateIsNull()
+ {
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn([]);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $this->assertNull($this->instance->state());
+ }
+
+ public function testUpdate()
+ {
+ $instance = $this->getDefaultInstance();
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn($instance);
+
+ $this->connection->updateInstance([
+ 'name' => $instance['name'],
+ 'displayName' => $instance['displayName'],
+ 'nodeCount' => $instance['nodeCount'],
+ 'labels' => [],
+ ])->shouldBeCalled();
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $this->instance->update();
+ }
+
+ public function testUpdateWithExistingLabels()
+ {
+ $instance = $this->getDefaultInstance();
+ $instance['labels'] = ['foo' => 'bar'];
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn($instance);
+
+ $this->connection->updateInstance([
+ 'name' => $instance['name'],
+ 'displayName' => $instance['displayName'],
+ 'nodeCount' => $instance['nodeCount'],
+ 'labels' => $instance['labels'],
+ ])->shouldBeCalled();
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $this->instance->update();
+ }
+
+ public function testUpdateWithChanges()
+ {
+ $instance = $this->getDefaultInstance();
+
+ $changes = [
+ 'labels' => [
+ 'foo' => 'bar'
+ ],
+ 'nodeCount' => 900,
+ 'displayName' => 'New Name',
+ ];
+
+ $this->connection->getInstance(Argument::any())
+ ->shouldBeCalledTimes(1)
+ ->willReturn($instance);
+
+ $this->connection->updateInstance([
+ 'name' => $instance['name'],
+ 'displayName' => $changes['displayName'],
+ 'nodeCount' => $changes['nodeCount'],
+ 'labels' => $changes['labels'],
+ ])->shouldBeCalled();
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $this->instance->update($changes);
+ }
+
+ public function testDelete()
+ {
+ $this->connection->deleteInstance([
+ 'name' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME)
+ ])->shouldBeCalled();
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $this->instance->delete();
+ }
+
+ public function testCreateDatabase()
+ {
+ $extra = ['foo', 'bar'];
+
+ $this->connection->createDatabase([
+ 'instance' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME),
+ 'createStatement' => 'CREATE DATABASE `test-database`',
+ 'extraStatements' => $extra
+ ])
+ ->shouldBeCalled()
+ ->willReturn(['name' => 'operations/foo']);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $database = $this->instance->createDatabase('test-database', [
+ 'statements' => $extra
+ ]);
+
+ $this->assertInstanceOf(LongRunningOperation::class, $database);
+ }
+
+ public function testDatabase()
+ {
+ $database = $this->instance->database('test-database');
+ $this->assertInstanceOf(Database::class, $database);
+ $this->assertEquals('test-database', $database->name());
+ }
+
+ public function testDatabases()
+ {
+ $databases = [
+ ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')],
+ ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')]
+ ];
+
+ $this->connection->listDatabases(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['databases' => $databases]);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $dbs = $this->instance->databases();
+
+ $this->assertInstanceOf(ItemIterator::class, $dbs);
+
+ $dbs = iterator_to_array($dbs);
+
+ $this->assertEquals(2, count($dbs));
+ $this->assertEquals('database1', $dbs[0]->name());
+ $this->assertEquals('database2', $dbs[1]->name());
+ }
+
+ public function testDatabasesPaged()
+ {
+ $databases = [
+ ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')],
+ ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')]
+ ];
+
+ $iteration = 0;
+ $this->connection->listDatabases(Argument::any())
+ ->shouldBeCalledTimes(2)
+ ->willReturn(['databases' => [$databases[0]], 'nextPageToken' => 'foo'], ['databases' => [$databases[1]]]);
+
+ $this->instance->___setProperty('connection', $this->connection->reveal());
+
+ $dbs = $this->instance->databases();
+
+ $this->assertInstanceOf(ItemIterator::class, $dbs);
+
+ $dbs = iterator_to_array($dbs);
+
+ $this->assertEquals(2, count($dbs));
+ $this->assertEquals('database1', $dbs[0]->name());
+ $this->assertEquals('database2', $dbs[1]->name());
+ }
+
+ public function testIam()
+ {
+ $this->assertInstanceOf(Iam::class, $this->instance->iam());
+ }
+
+ // ************** //
+
+ private function getDefaultInstance()
+ {
+ return json_decode(file_get_contents(__DIR__ .'/../fixtures/spanner/instance.json'), true);
+ }
+}
diff --git a/tests/unit/Spanner/KeyRangeTest.php b/tests/unit/Spanner/KeyRangeTest.php
new file mode 100644
index 000000000000..c276ba677d5e
--- /dev/null
+++ b/tests/unit/Spanner/KeyRangeTest.php
@@ -0,0 +1,95 @@
+range = new KeyRange;
+ }
+
+ public function testGetters()
+ {
+ $range = new KeyRange([
+ 'startType' => KeyRange::TYPE_CLOSED,
+ 'start' => ['foo'],
+ 'endType' => KeyRange::TYPE_OPEN,
+ 'end' => ['bar']
+ ]);
+
+ $this->assertEquals(['foo'], $range->start());
+ $this->assertEquals(['bar'], $range->end());
+ $this->assertEquals(['start' => KeyRange::TYPE_CLOSED, 'end' => KeyRange::TYPE_OPEN], $range->types());
+ }
+
+ public function testSetStart()
+ {
+ $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']);
+ $this->assertEquals(['foo'], $this->range->start());
+ $this->assertEquals(KeyRange::TYPE_OPEN, $this->range->types()['start']);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testSetStartInvalidType()
+ {
+ $this->range->setStart('foo', ['foo']);
+ }
+
+ public function testSetEnd()
+ {
+ $this->range->setEnd(KeyRange::TYPE_OPEN, ['foo']);
+ $this->assertEquals(['foo'], $this->range->end());
+ $this->assertEquals(KeyRange::TYPE_OPEN, $this->range->types()['end']);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testSetEndInvalidType()
+ {
+ $this->range->setEnd('foo', ['foo']);
+ }
+
+ public function testKeyRangeObject()
+ {
+ $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']);
+ $this->range->setEnd(KeyRange::TYPE_CLOSED, ['bar']);
+
+ $res = $this->range->keyRangeObject();
+
+ $this->assertEquals(['startOpen' => ['foo'], 'endClosed' => ['bar']], $res);
+ }
+
+ /**
+ * @expectedException BadMethodCallException
+ */
+ public function testKeyRangeObjectBadRange()
+ {
+ $this->range->keyRangeObject();
+ }
+}
diff --git a/tests/unit/Spanner/KeySetTest.php b/tests/unit/Spanner/KeySetTest.php
new file mode 100644
index 000000000000..75551a6dc946
--- /dev/null
+++ b/tests/unit/Spanner/KeySetTest.php
@@ -0,0 +1,119 @@
+prophesize(KeyRange::class);
+ $range->keyRangeObject()->willReturn('foo');
+
+ $set->addRange($range->reveal());
+
+ $this->assertEquals('foo', $set->keySetObject()['ranges'][0]);
+ }
+
+ public function testSetRanges()
+ {
+ $set = new KeySet;
+
+ $range1 = $this->prophesize(KeyRange::class);
+ $range1->keyRangeObject()->willReturn('foo');
+
+ $range2 = $this->prophesize(KeyRange::class);
+ $range2->keyRangeObject()->willReturn('bar');
+
+ $ranges = [
+ $range1->reveal(),
+ $range2->reveal()
+ ];
+
+ $set->setRanges($ranges);
+
+ $this->assertEquals('foo', $set->keySetObject()['ranges'][0]);
+ $this->assertEquals('bar', $set->keySetObject()['ranges'][1]);
+ }
+
+ public function testAddKey()
+ {
+ $set = new KeySet;
+
+ $key = 'key';
+
+ $set->addKey($key);
+
+ $this->assertEquals($key, $set->keySetObject()['keys'][0]);
+ }
+
+ public function testSetKeys()
+ {
+ $set = new KeySet;
+
+ $keys = ['key1','key2'];
+
+ $set->setKeys($keys);
+
+ $this->assertEquals($keys, $set->keySetObject()['keys']);
+ }
+
+ public function testSetMatchAll()
+ {
+ $set = new KeySet;
+
+ $set->setMatchAll(true);
+ $this->assertTrue($set->keySetObject()['all']);
+
+ $set->setMatchAll(false);
+ $this->assertFalse($set->keySetObject()['all']);
+ }
+
+ public function testRanges()
+ {
+ $set = new KeySet;
+ $range = $this->prophesize(KeyRange::class)->reveal();
+
+ $set->addRange($range);
+ $this->assertEquals($range, $set->ranges()[0]);
+ }
+
+ public function testKeys()
+ {
+ $set = new KeySet;
+ $key = 'foo';
+ $set->addKey($key);
+
+ $this->assertEquals($key, $set->keys()[0]);
+ }
+
+ public function testMatchAll()
+ {
+ $set = new KeySet();
+ $this->assertFalse($set->matchAll());
+
+ $set->setMatchAll(true);
+ $this->assertTrue($set->matchAll());
+ }
+}
diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php
new file mode 100644
index 000000000000..298e8531632a
--- /dev/null
+++ b/tests/unit/Spanner/OperationTest.php
@@ -0,0 +1,303 @@
+connection = $this->prophesize(ConnectionInterface::class);
+
+ $this->operation = \Google\Cloud\Dev\stub(Operation::class, [
+ $this->connection->reveal(),
+ false
+ ]);
+
+ $session = $this->prophesize(Session::class);
+ $session->name()->willReturn(self::SESSION);
+ $this->session = $session->reveal();
+ }
+
+ public function testMutation()
+ {
+ $res = $this->operation->mutation(Operation::OP_INSERT, 'Posts', [
+ 'foo' => 'bar'
+ ]);
+
+ $this->assertEquals(Operation::OP_INSERT, array_keys($res)[0]);
+ $this->assertEquals('Posts', $res[Operation::OP_INSERT]['table']);
+ $this->assertEquals('foo', $res[Operation::OP_INSERT]['columns'][0]);
+ $this->assertEquals('bar', $res[Operation::OP_INSERT]['values'][0]);
+ }
+
+ public function testDeleteMutation()
+ {
+ $keys = ['foo', 'bar'];
+ $range = new KeyRange([
+ 'startType' => KeyRange::TYPE_CLOSED,
+ 'start' => ['foo'],
+ 'endType' => KeyRange::TYPE_OPEN,
+ 'end' => ['bar']
+ ]);
+
+ $keySet = new KeySet([
+ 'keys' => $keys,
+ 'ranges' => [$range]
+ ]);
+
+ $res = $this->operation->deleteMutation('Posts', $keySet);
+
+ $this->assertEquals('Posts', $res['delete']['table']);
+ $this->assertEquals($keys, $res['delete']['keySet']['keys']);
+ $this->assertEquals($range->keyRangeObject(), $res['delete']['keySet']['ranges'][0]);
+ }
+
+ public function testCommit()
+ {
+ $mutations = [
+ $this->operation->mutation(Operation::OP_INSERT, 'Posts', [
+ 'foo' => 'bar'
+ ])
+ ];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($mutations) {
+ if ($arg['mutations'] !== $mutations) return false;
+ if ($arg['transactionId'] !== 'foo') return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]);
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->operation->commit($this->session, $mutations, [
+ 'transactionId' => 'foo'
+ ]);
+
+ $this->assertInstanceOf(Timestamp::class, $res);
+ }
+
+ public function testCommitWithExistingTransaction()
+ {
+ $mutations = [
+ $this->operation->mutation(Operation::OP_INSERT, 'Posts', [
+ 'foo' => 'bar'
+ ])
+ ];
+
+ $this->connection->commit(Argument::that(function ($arg) use ($mutations) {
+ if ($arg['mutations'] !== $mutations) return false;
+ if (isset($arg['singleUseTransaction'])) return false;
+ if ($arg['transactionId'] !== self::TRANSACTION) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]);
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->operation->commit($this->session, $mutations, [
+ 'transactionId' => self::TRANSACTION
+ ]);
+
+ $this->assertInstanceOf(Timestamp::class, $res);
+ }
+
+ public function testRollback()
+ {
+ $this->connection->rollback(Argument::that(function ($arg) {
+ if ($arg['transactionId'] !== self::TRANSACTION) return false;
+ if ($arg['session'] !== self::SESSION) return false;
+
+ return true;
+ }))->shouldBeCalled();
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $this->operation->rollback($this->session, self::TRANSACTION);
+ }
+
+ public function testExecute()
+ {
+ $sql = 'SELECT * FROM Posts WHERE ID = @id';
+ $params = ['id' => 10];
+
+ $this->connection->executeSql(Argument::that(function ($arg) use ($sql, $params) {
+ if ($arg['sql'] !== $sql) return false;
+ if ($arg['session'] !== self::SESSION) return false;
+ if ($arg['params'] !== ['id' => '10']) return false;
+ if ($arg['paramTypes']['id']['code'] !== ValueMapper::TYPE_INT64) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->executeAndReadResponse());
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->operation->execute($this->session, $sql, [
+ 'parameters' => $params
+ ]);
+
+ $this->assertInstanceOf(Result::class, $res);
+ $this->assertEquals(10, $res->rows()[0]['ID']);
+ }
+
+ public function testRead()
+ {
+ $this->connection->read(Argument::that(function ($arg) {
+ if ($arg['table'] !== 'Posts') return false;
+ if ($arg['session'] !== self::SESSION) return false;
+ if ($arg['keySet']['all'] !== true) return false;
+ if ($arg['columns'] !== ['foo']) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->executeAndReadResponse());
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo']);
+ $this->assertInstanceOf(Result::class, $res);
+ $this->assertEquals(10, $res->rows()[0]['ID']);
+ }
+
+ public function testReadWithTransaction()
+ {
+ $this->connection->read(Argument::that(function ($arg) {
+ if ($arg['table'] !== 'Posts') return false;
+ if ($arg['session'] !== self::SESSION) return false;
+ if ($arg['keySet']['all'] !== true) return false;
+ if ($arg['columns'] !== ['foo']) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->executeAndReadResponse([
+ 'transaction' => ['id' => self::TRANSACTION]
+ ]));
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [
+ 'transactionContext' => SessionPoolInterface::CONTEXT_READWRITE
+ ]);
+ $this->assertInstanceOf(Transaction::class, $res->transaction());
+ $this->assertEquals(self::TRANSACTION, $res->transaction()->id());
+ }
+
+ public function testReadWithSnapshot()
+ {
+ $this->connection->read(Argument::that(function ($arg) {
+ if ($arg['table'] !== 'Posts') return false;
+ if ($arg['session'] !== self::SESSION) return false;
+ if ($arg['keySet']['all'] !== true) return false;
+ if ($arg['columns'] !== ['foo']) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn($this->executeAndReadResponse([
+ 'transaction' => ['id' => self::TRANSACTION]
+ ]));
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [
+ 'transactionContext' => SessionPoolInterface::CONTEXT_READ
+ ]);
+ $this->assertInstanceOf(Snapshot::class, $res->snapshot());
+ $this->assertEquals(self::TRANSACTION, $res->snapshot()->id());
+ }
+
+ public function testTransaction()
+ {
+ $this->connection->beginTransaction(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['id' => self::TRANSACTION]);
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $t = $this->operation->transaction($this->session);
+ $this->assertInstanceOf(Transaction::class, $t);
+ $this->assertEquals(self::TRANSACTION, $t->id());
+ }
+
+ public function testSnapshot()
+ {
+ $this->connection->beginTransaction(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['id' => self::TRANSACTION]);
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $snap = $this->operation->snapshot($this->session);
+ $this->assertInstanceOf(Snapshot::class, $snap);
+ $this->assertEquals(self::TRANSACTION, $snap->id());
+ }
+
+ public function testSnapshotWithTimestamp()
+ {
+ $this->connection->beginTransaction(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn(['id' => self::TRANSACTION, 'readTimestamp' => self::TIMESTAMP]);
+
+ $this->operation->___setProperty('connection', $this->connection->reveal());
+
+ $snap = $this->operation->snapshot($this->session);
+ $this->assertInstanceOf(Snapshot::class, $snap);
+ $this->assertEquals(self::TRANSACTION, $snap->id());
+ $this->assertInstanceOf(Timestamp::class, $snap->readTimestamp());
+ }
+
+ private function executeAndReadResponse(array $additionalMetadata = [])
+ {
+ return [
+ 'metadata' => array_merge([
+ 'rowType' => [
+ 'fields' => [
+ [
+ 'name' => 'ID',
+ 'type' => [
+ 'code' => ValueMapper::TYPE_INT64
+ ]
+ ]
+ ]
+ ]
+ ], $additionalMetadata),
+ 'rows' => [
+ ['10']
+ ]
+ ];
+ }
+}
diff --git a/tests/unit/Spanner/ResultTest.php b/tests/unit/Spanner/ResultTest.php
new file mode 100644
index 000000000000..98fa6d8ef6b5
--- /dev/null
+++ b/tests/unit/Spanner/ResultTest.php
@@ -0,0 +1,106 @@
+ 'John']
+ ]);
+
+ $res = iterator_to_array($result);
+ $this->assertEquals(1, count($res));
+ $this->assertEquals('John', $res[0]['name']);
+ }
+
+ public function testMetadata()
+ {
+ $result = new Result(['metadata' => 'foo'], []);
+ $this->assertEquals('foo', $result->metadata());
+ }
+
+ public function testRows()
+ {
+ $rows = [
+ ['name' => 'John']
+ ];
+
+ $result = new Result([], $rows);
+
+ $this->assertEquals($rows, $result->rows());
+ }
+
+ public function testFirstRow()
+ {
+ $rows = [
+ ['name' => 'John'],
+ ['name' => 'Dave']
+ ];
+
+ $result = new Result([], $rows);
+
+ $this->assertEquals($rows[0], $result->firstRow());
+ }
+
+ public function testStats()
+ {
+ $result = new Result(['stats' => 'foo'], []);
+ $this->assertEquals('foo', $result->stats());
+ }
+
+ public function testInfo()
+ {
+ $info = ['foo' => 'bar'];
+ $result = new Result($info, []);
+
+ $this->assertEquals($info, $result->info());
+ }
+
+ public function testTransaction()
+ {
+ $result = new Result([], [], [
+ 'transaction' => 'foo'
+ ]);
+
+ $this->assertEquals('foo', $result->transaction());
+
+ $result = new Result([], []);
+
+ $this->assertNull($result->transaction());
+ }
+
+ public function testSnapshot()
+ {
+ $result = new Result([], [], [
+ 'snapshot' => 'foo'
+ ]);
+
+ $this->assertEquals('foo', $result->snapshot());
+
+ $result = new Result([], []);
+
+ $this->assertNull($result->snapshot());
+ }
+}
diff --git a/tests/unit/Spanner/SnapshotTest.php b/tests/unit/Spanner/SnapshotTest.php
new file mode 100644
index 000000000000..cf54179ce9ed
--- /dev/null
+++ b/tests/unit/Spanner/SnapshotTest.php
@@ -0,0 +1,48 @@
+timestamp = new Timestamp(new \DateTime);
+ $this->snapshot = new Snapshot(
+ $this->prophesize(Operation::class)->reveal(),
+ $this->prophesize(Session::class)->reveal(),
+ 'foo',
+ $this->timestamp
+ );
+ }
+
+ public function testReadTimestamp()
+ {
+ $this->assertEquals($this->timestamp, $this->snapshot->readTimestamp());
+ }
+}
diff --git a/tests/unit/Spanner/SpannerClientTest.php b/tests/unit/Spanner/SpannerClientTest.php
new file mode 100644
index 000000000000..8c0fccfa497c
--- /dev/null
+++ b/tests/unit/Spanner/SpannerClientTest.php
@@ -0,0 +1,284 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->client = \Google\Cloud\Dev\stub(SpannerClient::class, [
+ ['projectId' => self::PROJECT]
+ ]);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testConfigurations()
+ {
+ $this->connection->listConfigs(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'instanceConfigs' => [
+ [
+ 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG,
+ 'displayName' => 'Bar'
+ ], [
+ 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG,
+ 'displayName' => 'Bat'
+ ]
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $configs = $this->client->configurations();
+
+ $this->assertInstanceOf(ItemIterator::class, $configs);
+
+ $configs = iterator_to_array($configs);
+ $this->assertEquals(2, count($configs));
+ $this->assertInstanceOf(Configuration::class, $configs[0]);
+ $this->assertInstanceOf(Configuration::class, $configs[1]);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testPagedConfigurations()
+ {
+ $firstCall = [
+ 'instanceConfigs' => [
+ [
+ 'name' => 'projects/foo/instanceConfigs/bar',
+ 'displayName' => 'Bar'
+ ]
+ ],
+ 'nextPageToken' => 'fooBar'
+ ];
+
+ $secondCall = [
+ 'instanceConfigs' => [
+ [
+ 'name' => 'projects/foo/instanceConfigs/bat',
+ 'displayName' => 'Bat'
+ ]
+ ]
+ ];
+
+ $this->connection->listConfigs(Argument::any())
+ ->shouldBeCalledTimes(2)
+ ->willReturn($firstCall, $secondCall);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $configs = $this->client->configurations();
+
+ $this->assertInstanceOf(ItemIterator::class, $configs);
+
+ $configs = iterator_to_array($configs);
+ $this->assertEquals(2, count($configs));
+ $this->assertInstanceOf(Configuration::class, $configs[0]);
+ $this->assertInstanceOf(Configuration::class, $configs[1]);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testConfiguration()
+ {
+ $config = $this->client->configuration('bar');
+
+ $this->assertInstanceOf(Configuration::class, $config);
+ $this->assertEquals('bar', $config->name());
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testCreateInstance()
+ {
+ $this->connection->createInstance(Argument::that(function ($arg) {
+ if ($arg['name'] !== 'projects/'. self::PROJECT .'/instances/'. self::INSTANCE) return false;
+ if ($arg['config'] !== 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG) return false;
+
+ return true;
+ }))
+ ->shouldBeCalled()
+ ->willReturn([
+ 'name' => 'operations/foo'
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $config = $this->prophesize(Configuration::class);
+ $config->name()->willReturn(self::CONFIG);
+
+ $operation = $this->client->createInstance($config->reveal(), self::INSTANCE);
+
+ $this->assertInstanceOf(LongRunningOperation::class, $operation);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testInstance()
+ {
+ $i = $this->client->instance('foo');
+ $this->assertInstanceOf(Instance::class, $i);
+ $this->assertEquals('foo', $i->name());
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testInstanceWithInstanceArray()
+ {
+ $i = $this->client->instance('foo', ['key' => 'val']);
+ $this->assertEquals('val', $i->info()['key']);
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testInstances()
+ {
+ $this->connection->listInstances(Argument::any())
+ ->shouldBeCalled()
+ ->willReturn([
+ 'instances' => [
+ ['name' => 'projects/test-project/instances/foo'],
+ ['name' => 'projects/test-project/instances/bar'],
+ ]
+ ]);
+
+ $this->client->___setProperty('connection', $this->connection->reveal());
+
+ $instances = $this->client->instances();
+ $this->assertInstanceOf(ItemIterator::class, $instances);
+
+ $instances = iterator_to_array($instances);
+ $this->assertEquals(2, count($instances));
+ $this->assertEquals('foo', $instances[0]->name());
+ $this->assertEquals('bar', $instances[1]->name());
+ }
+
+ /**
+ * @group spanneradmin
+ */
+ public function testResumeOperation()
+ {
+ $opName = 'operations/foo';
+
+ $op = $this->client->resumeOperation($opName);
+ $this->assertInstanceOf(LongRunningOperation::class, $op);
+ $this->assertEquals($op->name(), $opName);
+ }
+
+ public function testConnect()
+ {
+ $database = $this->client->connect(self::INSTANCE, self::DATABASE);
+ $this->assertInstanceOf(Database::class, $database);
+ $this->assertEquals(self::DATABASE, $database->name());
+ }
+
+ public function testConnectWithInstance()
+ {
+ $inst = $this->client->instance(self::INSTANCE);
+ $database = $this->client->connect($inst, self::DATABASE);
+ $this->assertInstanceOf(Database::class, $database);
+ $this->assertEquals(self::DATABASE, $database->name());
+ }
+
+ public function testKeyset()
+ {
+ $ks = $this->client->keySet();
+ $this->assertInstanceOf(KeySet::class, $ks);
+ }
+
+ public function testKeyRange()
+ {
+ $kr = $this->client->keyRange();
+ $this->assertInstanceOf(KeyRange::class, $kr);
+ }
+
+ public function testBytes()
+ {
+ $b = $this->client->bytes('foo');
+ $this->assertInstanceOf(Bytes::class, $b);
+ $this->assertEquals(base64_encode('foo'), (string)$b);
+ }
+
+ public function testDate()
+ {
+ $d = $this->client->date(new \DateTime);
+ $this->assertInstanceOf(Date::class, $d);
+ }
+
+ public function testTimestamp()
+ {
+ $ts = $this->client->timestamp(new \DateTime);
+ $this->assertInstanceOf(Timestamp::class, $ts);
+ }
+
+ public function testInt64()
+ {
+ $i64 = $this->client->int64('123');
+ $this->assertInstanceOf(Int64::class, $i64);
+ }
+
+ public function testDuration()
+ {
+ $d = $this->client->duration(10, 1);
+ $this->assertInstanceOf(Duration::class, $d);
+ }
+
+ public function testSessionClient()
+ {
+ $sc = $this->client->sessionClient();
+ $this->assertInstanceOf(SessionClient::class, $sc);
+ }
+}
diff --git a/tests/unit/Spanner/TimestampTest.php b/tests/unit/Spanner/TimestampTest.php
new file mode 100644
index 000000000000..c18ee94e95c2
--- /dev/null
+++ b/tests/unit/Spanner/TimestampTest.php
@@ -0,0 +1,61 @@
+dt = new \DateTime('1989-10-11 08:58:00 +00:00');
+ $this->ts = new Timestamp($this->dt);
+ }
+
+ public function testGet()
+ {
+ $this->assertEquals($this->dt, $this->ts->get());
+ }
+
+ public function testFormatAsString()
+ {
+ $this->assertEquals(
+ (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'),
+ (new \DateTime($this->ts->formatAsString()))->format('U')
+ );
+ }
+
+ public function testCast()
+ {
+ $this->assertEquals(
+ (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'),
+ (new \DateTime((string)$this->ts))->format('U')
+ );
+ }
+
+ public function testType()
+ {
+ $this->assertTrue(is_integer($this->ts->type()));
+ }
+}
diff --git a/tests/unit/Spanner/TransactionConfigurationTraitTest.php b/tests/unit/Spanner/TransactionConfigurationTraitTest.php
new file mode 100644
index 000000000000..2dd3f408e0b3
--- /dev/null
+++ b/tests/unit/Spanner/TransactionConfigurationTraitTest.php
@@ -0,0 +1,185 @@
+impl = new TransactionConfigurationTraitImplementation;
+ $this->ts = new Timestamp(new \DateTime(self::TIMESTAMP), self::NANOS);
+ $this->duration = new Duration(10,1);
+ $this->dur = ['seconds' => 10, 'nanos' => 1];
+ }
+
+ public function testTransactionSelectorBasicSnapshot()
+ {
+ $args = [];
+ $res = $this->impl->proxyTransactionSelector($args);
+ $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]);
+ $this->assertTrue($res[0]['singleUse']['readOnly']['strong']);
+ }
+
+ public function testTransactionSelectorExistingId()
+ {
+ $args = ['transactionId' => self::TRANSACTION];
+ $res = $this->impl->proxyTransactionSelector($args);
+ $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]);
+ $this->assertEquals(self::TRANSACTION, $res[0]['id']);
+ }
+
+ public function testTransactionSelectorReadWrite()
+ {
+ $args = ['transactionType' => SessionPoolInterface::CONTEXT_READWRITE];
+ $res = $this->impl->proxyTransactionSelector($args);
+ $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $res[1]);
+ $this->assertEquals($this->impl->proxyConfigureTransactionOptions(), $res[0]['singleUse']);
+ }
+
+ public function testBegin()
+ {
+ $args = ['begin' => true];
+ $res = $this->impl->proxyTransactionSelector($args);
+ $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]);
+ $this->assertTrue($res[0]['begin']['readOnly']['strong']);
+ }
+
+ public function testConfigureSnapshotOptionsReturnReadTimestamp()
+ {
+ $args = ['returnReadTimestamp' => true];
+ $res = $this->impl->proxyConfigureSnapshotOptions($args);
+ $this->assertTrue($res['readOnly']['returnReadTimestamp']);
+ }
+
+ public function testConfigureSnapshotOptionsStrong()
+ {
+ $args = ['strong' => true];
+ $res = $this->impl->proxyConfigureSnapshotOptions($args);
+ $this->assertTrue($res['readOnly']['strong']);
+ }
+
+ public function testConfigureSnapshotOptionsMinReadTimestamp()
+ {
+ $args = ['minReadTimestamp' => $this->ts];
+ $res = $this->impl->proxyConfigureSnapshotOptions($args);
+ $this->assertEquals(self::TIMESTAMP, $res['readOnly']['minReadTimestamp']);
+ }
+
+ public function testConfigureSnapshotOptionsReadTimestamp()
+ {
+ $args = ['readTimestamp' => $this->ts];
+ $res = $this->impl->proxyConfigureSnapshotOptions($args);
+ $this->assertEquals(self::TIMESTAMP, $res['readOnly']['readTimestamp']);
+ }
+
+ public function testConfigureSnapshotOptionsMaxStaleness()
+ {
+ $args = ['maxStaleness' => $this->duration];
+ $res = $this->impl->proxyConfigureSnapshotOptions($args);
+ $this->assertEquals($this->dur, $res['readOnly']['maxStaleness']);
+ }
+
+ public function testConfigureSnapshotOptionsExactStaleness()
+ {
+ $args = ['exactStaleness' => $this->duration];
+ $res = $this->impl->proxyConfigureSnapshotOptions($args);
+ $this->assertEquals($this->dur, $res['readOnly']['exactStaleness']);
+ }
+
+ /**
+ * @expectedException BadMethodCallException
+ */
+ public function testTransactionSelectorInvalidContext()
+ {
+ $args = ['transactionType' => 'foo'];
+ $this->impl->proxyTransactionSelector($args);
+ }
+
+ /**
+ * @expectedException BadMethodCallException
+ */
+ public function testConfigureSnapshotOptionsInvalidExactStaleness()
+ {
+ $args = ['exactStaleness' => 'foo'];
+ $this->impl->proxyConfigureSnapshotOptions($args);
+ }
+
+ /**
+ * @expectedException BadMethodCallException
+ */
+ public function testConfigureSnapshotOptionsInvalidMaxStaleness()
+ {
+ $args = ['maxStaleness' => 'foo'];
+ $this->impl->proxyConfigureSnapshotOptions($args);
+ }
+
+ /**
+ * @expectedException BadMethodCallException
+ */
+ public function testConfigureSnapshotOptionsInvalidMinReadTimestamp()
+ {
+ $args = ['minReadTimestamp' => 'foo'];
+ $this->impl->proxyConfigureSnapshotOptions($args);
+ }
+
+ /**
+ * @expectedException BadMethodCallException
+ */
+ public function testConfigureSnapshotOptionsInvalidReadTimestamp()
+ {
+ $args = ['readTimestamp' => 'foo'];
+ $this->impl->proxyConfigureSnapshotOptions($args);
+ }
+}
+
+class TransactionConfigurationTraitImplementation
+{
+ use TransactionConfigurationTrait;
+
+ public function proxyTransactionSelector(array &$options)
+ {
+ return $this->transactionSelector($options);
+ }
+
+ public function proxyConfigureTransactionOptions()
+ {
+ return $this->configureTransactionOptions();
+ }
+
+ public function proxyConfigureSnapshotOptions(array &$options)
+ {
+ return $this->configureSnapshotOptions($options);
+ }
+}
diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php
new file mode 100644
index 000000000000..867ae378dbb2
--- /dev/null
+++ b/tests/unit/Spanner/TransactionTest.php
@@ -0,0 +1,324 @@
+connection = $this->prophesize(ConnectionInterface::class);
+ $this->operation = new Operation($this->connection->reveal(), false);
+
+ $this->session = new Session(
+ $this->connection->reveal(),
+ self::PROJECT,
+ self::INSTANCE,
+ self::DATABASE,
+ self::SESSION
+ );
+
+ $args = [
+ $this->operation,
+ $this->session,
+ self::TRANSACTION,
+ ];
+
+ $props = [
+ 'operation', 'readTimestamp', 'state'
+ ];
+
+ $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props);
+ }
+
+ public function testInsert()
+ {
+ $this->transaction->insert('Posts', ['foo' => 'bar']);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $this->assertEquals('Posts', $mutations[0]['insert']['table']);
+ $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]);
+ $this->assertEquals('bar', $mutations[0]['insert']['values'][0]);
+ }
+
+ public function testInsertBatch()
+ {
+ $this->transaction->insertBatch('Posts', [['foo' => 'bar']]);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $this->assertEquals('Posts', $mutations[0]['insert']['table']);
+ $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]);
+ $this->assertEquals('bar', $mutations[0]['insert']['values'][0]);
+ }
+
+ public function testUpdate()
+ {
+ $this->transaction->update('Posts', ['foo' => 'bar']);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $this->assertEquals('Posts', $mutations[0]['update']['table']);
+ $this->assertEquals('foo', $mutations[0]['update']['columns'][0]);
+ $this->assertEquals('bar', $mutations[0]['update']['values'][0]);
+ }
+
+ public function testUpdateBatch()
+ {
+ $this->transaction->updateBatch('Posts', [['foo' => 'bar']]);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $this->assertEquals('Posts', $mutations[0]['update']['table']);
+ $this->assertEquals('foo', $mutations[0]['update']['columns'][0]);
+ $this->assertEquals('bar', $mutations[0]['update']['values'][0]);
+ }
+
+ public function testInsertOrUpdate()
+ {
+ $this->transaction->insertOrUpdate('Posts', ['foo' => 'bar']);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $this->assertEquals('Posts', $mutations[0]['insertOrUpdate']['table']);
+ $this->assertEquals('foo', $mutations[0]['insertOrUpdate']['columns'][0]);
+ $this->assertEquals('bar', $mutations[0]['insertOrUpdate']['values'][0]);
+ }
+
+ public function testInsertOrUpdateBatch()
+ {
+ $this->transaction->insertOrUpdateBatch('Posts', [['foo' => 'bar']]);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $this->assertEquals('Posts', $mutations[0]['insertOrUpdate']['table']);
+ $this->assertEquals('foo', $mutations[0]['insertOrUpdate']['columns'][0]);
+ $this->assertEquals('bar', $mutations[0]['insertOrUpdate']['values'][0]);
+ }
+
+ public function testReplace()
+ {
+ $this->transaction->replace('Posts', ['foo' => 'bar']);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $this->assertEquals('Posts', $mutations[0]['replace']['table']);
+ $this->assertEquals('foo', $mutations[0]['replace']['columns'][0]);
+ $this->assertEquals('bar', $mutations[0]['replace']['values'][0]);
+ }
+
+ public function testReplaceBatch()
+ {
+ $this->transaction->replaceBatch('Posts', [['foo' => 'bar']]);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $this->assertEquals('Posts', $mutations[0]['replace']['table']);
+ $this->assertEquals('foo', $mutations[0]['replace']['columns'][0]);
+ $this->assertEquals('bar', $mutations[0]['replace']['values'][0]);
+ }
+
+ public function testDelete()
+ {
+ $this->transaction->delete('Posts', new KeySet(['keys' => ['foo']]));
+
+ $mutations = $this->transaction->___getProperty('mutations');
+ $this->assertEquals('Posts', $mutations[0]['delete']['table']);
+ $this->assertEquals('foo', $mutations[0]['delete']['keySet']['keys'][0]);
+ $this->assertFalse($mutations[0]['delete']['keySet']['all']);
+ }
+
+ public function testExecute()
+ {
+ $sql = 'SELECT * FROM Table';
+
+ $this->connection->executeSql(Argument::that(function ($arg) use ($sql) {
+ if ($arg['transaction']['id'] !== self::TRANSACTION) return false;
+ if ($arg['sql'] !== $sql) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn([
+ 'metadata' => [
+ 'rowType' => [
+ 'fields' => [
+ [
+ 'name' => 'ID',
+ 'type' => [
+ 'code' => ValueMapper::TYPE_INT64
+ ]
+ ]
+ ]
+ ]
+ ],
+ 'rows' => [
+ [
+ '10'
+ ]
+ ]
+ ]);
+
+ $this->refreshOperation();
+
+ $res = $this->transaction->execute($sql);
+ $this->assertInstanceOf(Result::class, $res);
+ $this->assertEquals(10, $res->rows()[0]['ID']);
+ }
+
+ public function testRead()
+ {
+ $table = 'Table';
+ $opts = ['foo' => 'bar'];
+
+ $this->connection->read(Argument::that(function ($arg) use ($table, $opts) {
+ if ($arg['transaction']['id'] !== self::TRANSACTION) return false;
+ if ($arg['table'] !== $table) return false;
+ if ($arg['keySet']['all'] !== true) return false;
+ if ($arg['columns'] !== ['ID']) return false;
+
+ return true;
+ }))->shouldBeCalled()->willReturn([
+ 'metadata' => [
+ 'rowType' => [
+ 'fields' => [
+ [
+ 'name' => 'ID',
+ 'type' => [
+ 'code' => ValueMapper::TYPE_INT64
+ ]
+ ]
+ ]
+ ]
+ ],
+ 'rows' => [
+ [
+ '10'
+ ]
+ ]
+ ]);
+
+ $this->refreshOperation();
+
+ $res = $this->transaction->read($table, new KeySet(['all' => true]), ['ID']);
+ $this->assertInstanceOf(Result::class, $res);
+ $this->assertEquals(10, $res->rows()[0]['ID']);
+ }
+
+ public function testCommit()
+ {
+ $this->transaction->insert('Posts', ['foo' => 'bar']);
+
+ $mutations = $this->transaction->___getProperty('mutations');
+
+ $operation = $this->prophesize(Operation::class);
+ $operation->commit($this->session, $mutations, ['transactionId' => self::TRANSACTION])->shouldBeCalled();
+
+ $this->transaction->___setProperty('operation', $operation->reveal());
+
+ $this->transaction->commit();
+ }
+
+ /**
+ * @expectedException RuntimeException
+ */
+ public function testCommitInvalidState()
+ {
+ $this->transaction->___setProperty('state', 'foo');
+ $this->transaction->commit();
+ }
+
+ public function testRollback()
+ {
+ $this->connection->rollback(Argument::any())
+ ->shouldBeCalled();
+
+ $this->refreshOperation();
+
+ $this->transaction->rollback();
+ }
+
+ /**
+ * @expectedException RuntimeException
+ */
+ public function testRollbackInvalidState()
+ {
+ $this->transaction->___setProperty('state', 'foo');
+ $this->transaction->rollback();
+ }
+
+ public function testId()
+ {
+ $this->assertEquals(self::TRANSACTION, $this->transaction->id());
+ }
+
+ public function testState()
+ {
+ $this->assertEquals(Transaction::STATE_ACTIVE, $this->transaction->state());
+
+ $this->transaction->___setProperty('state', Transaction::STATE_COMMITTED);
+ $this->assertEquals(Transaction::STATE_COMMITTED, $this->transaction->state());
+ }
+
+ // *******
+ // Helpers
+
+ private function refreshOperation()
+ {
+ $operation = new Operation($this->connection->reveal(), false);
+ $this->transaction->___setProperty('operation', $operation);
+ }
+
+ private function commitResponse()
+ {
+ return ['commitTimestamp' => self::TIMESTAMP];
+ }
+
+ private function assertTimestampIsCorrect($res)
+ {
+ $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']);
+
+ $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z'));
+ }
+}
diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php
new file mode 100644
index 000000000000..404ba6a3201e
--- /dev/null
+++ b/tests/unit/Spanner/ValueMapperTest.php
@@ -0,0 +1,385 @@
+mapper = new ValueMapper(false);
+ }
+
+ public function testFormatParamsForExecuteSqlSimpleTypes()
+ {
+ $params = [
+ 'id' => 1,
+ 'name' => 'john',
+ 'pi' => 3.1515,
+ 'isCool' => false,
+ ];
+
+ $res = $this->mapper->formatParamsForExecuteSql($params);
+
+ $this->assertEquals($params, $res['params']);
+ $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['id']['code']);
+ $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['name']['code']);
+ $this->assertEquals(ValueMapper::TYPE_FLOAT64, $res['paramTypes']['pi']['code']);
+ $this->assertEquals(ValueMapper::TYPE_BOOL, $res['paramTypes']['isCool']['code']);
+ }
+
+ public function testFormatParamsForExecuteSqlResource()
+ {
+ $c = 'hello world';
+
+ $resource = fopen('php://temp', 'r+');
+ fwrite($resource, $c);
+ rewind($resource);
+
+ $params = [
+ 'resource' => $resource
+ ];
+
+ $res = $this->mapper->formatParamsForExecuteSql($params);
+
+ $this->assertEquals($c, base64_decode($res['params']['resource']));
+ $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['resource']['code']);
+ }
+
+ public function testFormatParamsForExecuteSqlArray()
+ {
+ $params = [
+ 'array' => ['foo', 'bar']
+ ];
+
+ $res = $this->mapper->formatParamsForExecuteSql($params);
+
+ $this->assertEquals('foo', $res['params']['array'][0]);
+ $this->assertEquals('bar', $res['params']['array'][1]);
+ $this->assertEquals(ValueMapper::TYPE_ARRAY, $res['paramTypes']['array']['code']);
+ $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['array']['arrayElementType']['code']);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testFormatParamsForExecuteSqlArrayInvalidAssoc()
+ {
+ $this->mapper->formatParamsForExecuteSql(['array' => [
+ 'foo' => 'bar'
+ ]]);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testFormatParamsForExecuteSqlInvalidTypes()
+ {
+ $this->mapper->formatParamsForExecuteSql(['array' => ['foo', 3.1515]]);
+ }
+
+ public function testFormatParamsForExecuteSqlInt64()
+ {
+ $val = '1234';
+ $params = [
+ 'int' => new Int64($val)
+ ];
+
+ $res = $this->mapper->formatParamsForExecuteSql($params);
+
+ $this->assertEquals($val, $res['params']['int']);
+ $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['int']['code']);
+ }
+
+ public function testFormatParamsForExecuteSqlValueInterface()
+ {
+ $val = 'hello world';
+ $params = [
+ 'bytes' => new Bytes($val)
+ ];
+
+ $res = $this->mapper->formatParamsForExecuteSql($params);
+ $this->assertEquals($val, base64_decode($res['params']['bytes']));
+ $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['bytes']['code']);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testFormatParamsForExecuteSqlInvalidObjectType()
+ {
+ $params = [
+ 'bad' => $this
+ ];
+
+ $this->mapper->formatParamsForExecuteSql($params);
+ }
+
+ public function testEncodeValuesAsSimpleType()
+ {
+ $dt = new \DateTime;
+
+ $vals = [];
+ $vals['bool'] = true;
+ $vals['int'] = 555555;
+ $vals['intObj'] = new Int64((string) $vals['int']);
+ $vals['float'] = 3.1415;
+ $vals['nan'] = NAN;
+ $vals['inf'] = INF;
+ $vals['timestamp'] = new Timestamp($dt);
+ $vals['date'] = new Date($dt);
+ $vals['string'] = 'foo';
+ $vals['bytes'] = new Bytes('hello world');
+ $vals['array'] = ['foo', 'bar'];
+
+ $res = $this->mapper->encodeValuesAsSimpleType($vals);
+
+ $this->assertTrue($res[0]);
+ $this->assertEquals((string) $vals['int'], $res[1]);
+ $this->assertEquals((string) $vals['int'], $res[2]);
+ $this->assertEquals($vals['float'], $res[3]);
+ $this->assertTrue(is_nan($res[4]));
+ $this->assertEquals(INF, $res[5]);
+ $this->assertEquals($dt->format(Timestamp::FORMAT), $res[6]);
+ $this->assertEquals($dt->format(Date::FORMAT), $res[7]);
+ $this->assertEquals($vals['string'], $res[8]);
+ $this->assertEquals(base64_encode('hello world'), $res[9]);
+ $this->assertEquals($vals['array'], $res[10]);
+ }
+
+ public function testDecodeValuesBool()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_BOOL),
+ $this->createRow(false)
+ );
+ $this->assertEquals(false, $res['rowName']);
+ }
+
+ public function testDecodeValuesInt()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_INT64),
+ $this->createRow('555')
+ );
+ $this->assertEquals(555, $res['rowName']);
+ }
+
+ public function testDecodeValuesInt64Object()
+ {
+ $mapper = new ValueMapper(true);
+ $res = $mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_INT64),
+ $this->createRow('555')
+ );
+ $this->assertInstanceOf(Int64::class, $res['rowName']);
+ $this->assertEquals('555', $res['rowName']->get());
+ }
+
+ public function testDecodeValuesFloat()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_FLOAT64),
+ $this->createRow(3.1415)
+ );
+ $this->assertEquals(3.1415, $res['rowName']);
+ }
+
+ public function testDecodeValuesFloatNaN()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_FLOAT64),
+ $this->createRow('NaN')
+ );
+ $this->assertTrue(is_nan($res['rowName']));
+ }
+
+ public function testDecodeValuesFloatInfinity()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_FLOAT64),
+ $this->createRow('Infinity')
+ );
+
+ $this->assertTrue(is_infinite($res['rowName']));
+ $this->assertTrue($res['rowName'] > 0);
+ }
+
+ public function testDecodeValuesFloatNegativeInfinity()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_FLOAT64),
+ $this->createRow('-Infinity')
+ );
+
+ $this->assertTrue(is_infinite($res['rowName']));
+ $this->assertTrue($res['rowName'] < 0);
+ }
+
+ /**
+ * @expectedException RuntimeException
+ */
+ public function testDecodeValuesFloatError()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_FLOAT64),
+ $this->createRow('foo')
+ );
+ }
+
+ public function testDecodeValuesString()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_STRING),
+ $this->createRow('foo')
+ );
+ $this->assertEquals('foo', $res['rowName']);
+ }
+
+ public function testDecodeValuesTimestamp()
+ {
+ $dt = new \DateTime;
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_TIMESTAMP),
+ $this->createRow($dt->format(Timestamp::FORMAT))
+ );
+
+ $this->assertInstanceOf(Timestamp::class, $res['rowName']);
+ $this->assertEquals($dt->format(Timestamp::FORMAT), $res['rowName']->formatAsString());
+ }
+
+ public function testDecodeValuesDate()
+ {
+ $dt = new \DateTime;
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_DATE),
+ $this->createRow($dt->format(Date::FORMAT))
+ );
+
+ $this->assertInstanceOf(Date::class, $res['rowName']);
+ $this->assertEquals($dt->format(Date::FORMAT), $res['rowName']->formatAsString());
+ }
+
+ public function testDecodeValuesBytes()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_BYTES),
+ $this->createRow(base64_encode('hello world'))
+ );
+
+ $this->assertInstanceOf(Bytes::class, $res['rowName']);
+ $this->assertEquals('hello world', $res['rowName']->get());
+ }
+
+ public function testDecodeValuesArray()
+ {
+ $res = $this->mapper->decodeValues(
+ $this->createField(ValueMapper::TYPE_ARRAY, 'arrayElementType', [
+ 'code' => ValueMapper::TYPE_STRING
+ ]), $this->createRow(['foo', 'bar'])
+ );
+
+ $this->assertEquals('foo', $res['rowName'][0]);
+ $this->assertEquals('bar', $res['rowName'][1]);
+ }
+
+ public function testDecodeValuesStruct()
+ {
+ $field = [
+ 'name' => 'structTest',
+ 'type' => [
+ 'code' => ValueMapper::TYPE_ARRAY,
+ 'arrayElementType' => [
+ 'code' => ValueMapper::TYPE_STRUCT,
+ 'structType' => [
+ 'fields' => [
+ [
+ 'name' => 'rowName',
+ 'type' => [
+ 'code' => ValueMapper::TYPE_STRING
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $row = [
+ [
+ 'Hello World'
+ ]
+ ];
+
+ $res = $this->mapper->decodeValues(
+ [$field],
+ [$row]
+ );
+
+ $this->assertEquals('Hello World', $res['structTest'][0]['rowName']);
+ }
+
+ public function testDecodeValuesAnonymousField()
+ {
+ $fields = [
+ [
+ 'name' => 'ID',
+ 'type' => [
+ 'code' => ValueMapper::TYPE_INT64,
+ ]
+ ], [
+ 'type' => [
+ 'code' => ValueMapper::TYPE_STRING
+ ]
+ ]
+ ];
+
+ $row = ['1337', 'John'];
+
+ $res = $this->mapper->decodeValues($fields, $row);
+
+ $this->assertEquals('1337', $res['ID']);
+ $this->assertEquals('John', $res[1]);
+ }
+
+ private function createField($code, $type = null, array $typeObj = [])
+ {
+ return [[
+ 'name' => 'rowName',
+ 'type' => array_filter([
+ 'code' => $code,
+ $type => $typeObj
+ ])
+ ]];
+ }
+
+ private function createRow($val)
+ {
+ return [$val];
+ }
+}
diff --git a/tests/unit/Speech/Connection/RestTest.php b/tests/unit/Speech/Connection/RestTest.php
index fec3bf296e2c..32ca5c001b34 100644
--- a/tests/unit/Speech/Connection/RestTest.php
+++ b/tests/unit/Speech/Connection/RestTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Speech\Connection;
+namespace Google\Cloud\Tests\Unit\Speech\Connection;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\Speech\Connection\Rest;
+use Google\Cloud\Speech\SpeechClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
diff --git a/tests/unit/Speech/OperationTest.php b/tests/unit/Speech/OperationTest.php
index 9ada91bec9b1..5d77840dbbbc 100644
--- a/tests/unit/Speech/OperationTest.php
+++ b/tests/unit/Speech/OperationTest.php
@@ -15,9 +15,9 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Speech;
+namespace Google\Cloud\Tests\Unit\Speech;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Speech\Connection\ConnectionInterface;
use Google\Cloud\Speech\Operation;
use Prophecy\Argument;
diff --git a/tests/unit/Speech/SpeechClientTest.php b/tests/unit/Speech/SpeechClientTest.php
index 10a5902c2473..58dce6563465 100644
--- a/tests/unit/Speech/SpeechClientTest.php
+++ b/tests/unit/Speech/SpeechClientTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Speech;
+namespace Google\Cloud\Tests\Unit\Speech;
use Google\Cloud\Speech\Connection\ConnectionInterface;
use Google\Cloud\Speech\Operation;
@@ -114,10 +114,11 @@ public function audioProvider()
{
stream_wrapper_unregister('http');
stream_wrapper_register('http', HttpStreamWrapper::class);
+ $gcsUri = 'gs://bucket/object';
$amrMock = $this->prophesize(StorageObject::class);
- $amrMock->identity(Argument::any())->willReturn(['bucket' => 'bucket', 'object' => 'object.amr']);
+ $amrMock->gcsUri(Argument::any())->willReturn($gcsUri . '.amr');
$awbMock = $this->prophesize(StorageObject::class);
- $awbMock->identity(Argument::any())->willReturn(['bucket' => 'bucket', 'object' => 'object.awb']);
+ $awbMock->gcsUri(Argument::any())->willReturn($gcsUri . '.awb');
$audioPath = __DIR__ . '/../data/brooklyn.flac';
return [
@@ -202,6 +203,22 @@ public function audioProvider()
]
]
],
+ [
+ $gcsUri,
+ [
+ 'encoding' => 'FLAC',
+ 'sampleRate' => 16000
+ ],
+ [
+ 'audio' => [
+ 'uri' => $gcsUri
+ ],
+ 'config' => [
+ 'encoding' => 'FLAC',
+ 'sampleRate' => 16000
+ ]
+ ]
+ ],
[
fopen('http://www.example.com/file.flac', 'r'),
[
diff --git a/tests/unit/Storage/AclTest.php b/tests/unit/Storage/AclTest.php
index 815c5d9ebe53..06b3f0c8626b 100644
--- a/tests/unit/Storage/AclTest.php
+++ b/tests/unit/Storage/AclTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Storage;
+namespace Google\Cloud\Tests\Unit\Storage;
use Google\Cloud\Storage\Acl;
use Prophecy\Argument;
diff --git a/tests/unit/Storage/BucketTest.php b/tests/unit/Storage/BucketTest.php
index 4e50e91f8b96..2860989467ee 100644
--- a/tests/unit/Storage/BucketTest.php
+++ b/tests/unit/Storage/BucketTest.php
@@ -15,13 +15,16 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Storage;
+namespace Google\Cloud\Tests\Unit\Storage;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\ServerException;
+use Google\Cloud\Core\Exception\ServiceException;
+use Google\Cloud\Core\Upload\ResumableUploader;
+use Google\Cloud\Storage\Acl;
use Google\Cloud\Storage\Bucket;
use Google\Cloud\Storage\Connection\ConnectionInterface;
use Google\Cloud\Storage\StorageObject;
-use Google\Cloud\Upload\ResumableUploader;
use Prophecy\Argument;
/**
@@ -42,14 +45,14 @@ public function testGetsAcl()
{
$bucket = new Bucket($this->connection->reveal(), 'bucket');
- $this->assertInstanceOf('Google\Cloud\Storage\Acl', $bucket->acl());
+ $this->assertInstanceOf(Acl::class, $bucket->acl());
}
public function testGetsDefaultAcl()
{
$bucket = new Bucket($this->connection->reveal(), 'bucket');
- $this->assertInstanceOf('Google\Cloud\Storage\Acl', $bucket->defaultAcl());
+ $this->assertInstanceOf(Acl::class, $bucket->defaultAcl());
}
public function testDoesExistTrue()
@@ -78,7 +81,7 @@ public function testUploadData()
$bucket = new Bucket($this->connection->reveal(), 'bucket');
$this->assertInstanceOf(
- 'Google\Cloud\Storage\StorageObject',
+ StorageObject::class,
$bucket->upload('some data to upload', ['name' => 'data.txt'])
);
}
@@ -99,7 +102,7 @@ public function testGetResumableUploader()
$bucket = new Bucket($this->connection->reveal(), 'bucket');
$this->assertInstanceOf(
- 'Google\Cloud\Upload\ResumableUploader',
+ ResumableUploader::class,
$bucket->getResumableUploader('some data to upload', ['name' => 'data.txt'])
);
}
@@ -118,7 +121,7 @@ public function testGetObject()
{
$bucket = new Bucket($this->connection->reveal(), 'bucket');
- $this->assertInstanceOf('Google\Cloud\Storage\StorageObject', $bucket->object('peter-venkman.jpg'));
+ $this->assertInstanceOf(StorageObject::class, $bucket->object('peter-venkman.jpg'));
}
public function testInstantiateObjectWithOptions()
@@ -131,14 +134,17 @@ public function testInstantiateObjectWithOptions()
'encryptionKeySHA256' => '123'
]);
- $this->assertInstanceOf('Google\Cloud\Storage\StorageObject', $object);
+ $this->assertInstanceOf(StorageObject::class, $object);
}
public function testGetsObjectsWithoutToken()
{
$this->connection->listObjects(Argument::any())->willReturn([
'items' => [
- ['name' => 'file.txt']
+ [
+ 'name' => 'file.txt',
+ 'generation' => 'abc'
+ ]
]
]);
@@ -154,12 +160,18 @@ public function testGetsObjectsWithToken()
[
'nextPageToken' => 'token',
'items' => [
- ['name' => 'file.txt']
+ [
+ 'name' => 'file.txt',
+ 'generation' => 'abc'
+ ]
]
],
[
'items' => [
- ['name' => 'file2.txt']
+ [
+ 'name' => 'file2.txt',
+ 'generation' => 'def'
+ ]
]
]
);
@@ -311,4 +323,31 @@ public function testGetsName()
$this->assertEquals($name, $bucket->name());
}
+
+ public function testIsWritable()
+ {
+ $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader);
+ $this->resumableUploader->getResumeUri()->willReturn('http://some-uri/');
+ $bucket = new Bucket($this->connection->reveal(), $name = 'bucket');
+ $this->assertTrue($bucket->isWritable());
+ }
+
+ public function testIsWritableAccessDenied()
+ {
+ $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader);
+ $this->resumableUploader->getResumeUri()->willThrow(new ServiceException('access denied', 403));
+ $bucket = new Bucket($this->connection->reveal(), $name = 'bucket');
+ $this->assertFalse($bucket->isWritable());
+ }
+
+ /**
+ * @expectedException Google\Cloud\Core\Exception\ServerException
+ */
+ public function testIsWritableServerException()
+ {
+ $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader);
+ $this->resumableUploader->getResumeUri()->willThrow(new ServerException('maintainence'));
+ $bucket = new Bucket($this->connection->reveal(), $name = 'bucket');
+ $bucket->isWritable(); // raises exception
+ }
}
diff --git a/tests/unit/Storage/Connection/RestTest.php b/tests/unit/Storage/Connection/RestTest.php
index daab4fbcb7ce..d3c1813a93f4 100644
--- a/tests/unit/Storage/Connection/RestTest.php
+++ b/tests/unit/Storage/Connection/RestTest.php
@@ -15,9 +15,14 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Storage\Connection;
+namespace Google\Cloud\Tests\Unit\Storage\Connection;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
+use Google\Cloud\Core\Upload\MultipartUploader;
+use Google\Cloud\Core\Upload\ResumableUploader;
use Google\Cloud\Storage\Connection\Rest;
+use Google\Cloud\Storage\StorageClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
@@ -35,7 +40,7 @@ class RestTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
- $this->requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper');
+ $this->requestWrapper = $this->prophesize(RequestWrapper::class);
$this->successBody = '{"canI":"kickIt"}';
}
@@ -49,7 +54,7 @@ public function testCallBasicMethods($method)
$request = new Request('GET', '/somewhere');
$response = new Response(200, [], $this->successBody);
- $requestBuilder = $this->prophesize('Google\Cloud\RequestBuilder');
+ $requestBuilder = $this->prophesize(RequestBuilder::class);
$requestBuilder->build(
Argument::type('string'),
Argument::type('string'),
@@ -57,7 +62,7 @@ public function testCallBasicMethods($method)
)->willReturn($request);
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->willReturn($response);
@@ -101,7 +106,7 @@ public function testDownloadObject()
$response = new Response(200, [], $this->successBody);
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->will(
function ($args) use (&$actualRequest, $response) {
@@ -117,7 +122,7 @@ function ($args) use (&$actualRequest, $response) {
'bucket' => 'bigbucket',
'object' => 'myfile.txt',
'generation' => 100,
- 'httpOptions' => ['debug' => true],
+ 'restOptions' => ['debug' => true],
'retries' => 0
]);
@@ -143,7 +148,7 @@ public function testInsertObject(
$response = new Response(200, ['Location' => 'http://www.mordor.com'], $this->successBody);
$this->requestWrapper->send(
- Argument::type('Psr\Http\Message\RequestInterface'),
+ Argument::type(RequestInterface::class),
Argument::type('array')
)->will(
function ($args) use (&$actualRequest, $response) {
@@ -184,7 +189,7 @@ public function insertObjectProvider()
'predefinedAcl' => 'private',
'metadata' => ['contentType' => 'text/plain']
],
- 'Google\Cloud\Upload\ResumableUploader',
+ ResumableUploader::class,
'text/plain',
[
'md5Hash' => base64_encode(Psr7\hash($tempFile, 'md5', true)),
@@ -196,7 +201,7 @@ public function insertObjectProvider()
'data' => $logoFile,
'validate' => false
],
- 'Google\Cloud\Upload\MultipartUploader',
+ MultipartUploader::class,
'image/svg+xml',
[
'name' => 'logo.svg'
@@ -215,7 +220,7 @@ public function insertObjectProvider()
]
]
],
- 'Google\Cloud\Upload\ResumableUploader',
+ ResumableUploader::class,
'text/plain',
[
'name' => 'file.ext',
diff --git a/tests/unit/Storage/EncryptionTraitTest.php b/tests/unit/Storage/EncryptionTraitTest.php
index 288472b99954..db09f49355f8 100644
--- a/tests/unit/Storage/EncryptionTraitTest.php
+++ b/tests/unit/Storage/EncryptionTraitTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Storage;
+namespace Google\Cloud\Tests\Unit\Storage;
use Google\Cloud\Storage\EncryptionTrait;
@@ -52,7 +52,7 @@ public function encryptionProvider()
return [
[
[
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => $this->getEncryptionHeaders($key, $hash)
]
],
@@ -63,7 +63,7 @@ public function encryptionProvider()
],
[
[
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => $this->getEncryptionHeaders($key, $hash)
]
],
@@ -73,7 +73,7 @@ public function encryptionProvider()
],
[
[
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => array_merge(
$this->getEncryptionHeaders($destinationKey, $destinationHash),
$this->getCopySourceEncryptionHeaders($key, $hash)
@@ -90,14 +90,14 @@ public function encryptionProvider()
],
[
[
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => $this->getEncryptionHeaders($key, $hash) + ['hey' => 'dont clobber me']
]
],
[
'encryptionKey' => $key,
'encryptionKeySHA256' => $hash,
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => [
'hey' => 'dont clobber me'
]
diff --git a/tests/unit/Storage/ObjectIteratorTest.php b/tests/unit/Storage/ObjectIteratorTest.php
new file mode 100644
index 000000000000..efc7ec0bfc8c
--- /dev/null
+++ b/tests/unit/Storage/ObjectIteratorTest.php
@@ -0,0 +1,40 @@
+prophesize(ObjectPageIterator::class);
+ $pageIterator->prefixes()
+ ->willReturn($prefixes)
+ ->shouldBeCalledTimes(1);
+
+ $items = new ObjectIterator($pageIterator->reveal());
+
+ $this->assertEquals($prefixes, $items->prefixes());
+ }
+}
diff --git a/tests/unit/Storage/ObjectPageIteratorTest.php b/tests/unit/Storage/ObjectPageIteratorTest.php
new file mode 100644
index 000000000000..75fa2b297582
--- /dev/null
+++ b/tests/unit/Storage/ObjectPageIteratorTest.php
@@ -0,0 +1,53 @@
+ 'call']
+ );
+
+ $pagesArray = iterator_to_array($pages);
+
+ $this->assertEquals(self::$prefixes, $pages->prefixes());
+ $this->assertEquals(self::$items, $pagesArray[0]);
+ }
+
+ public function theCall(array $options)
+ {
+ return [
+ 'prefixes' => self::$prefixes,
+ 'items' => self::$items
+ ];
+ }
+}
diff --git a/tests/unit/Storage/ReadStreamTest.php b/tests/unit/Storage/ReadStreamTest.php
new file mode 100644
index 000000000000..0b64fde0d9b3
--- /dev/null
+++ b/tests/unit/Storage/ReadStreamTest.php
@@ -0,0 +1,71 @@
+prophesize('Psr\Http\Message\StreamInterface');
+ $httpStream->getSize()->willReturn(null);
+ $httpStream->getMetadata('wrapper_data')->willReturn([
+ "Foo: bar",
+ "User-Agent: php",
+ "Content-Length: 1234",
+ "Asdf: qwer",
+ ]);
+
+ $stream = new ReadStream($httpStream->reveal());
+
+ $this->assertEquals(1234, $stream->getSize());
+ }
+
+ public function testReadsFromHeadersWhenGetSizeIsZero()
+ {
+ $httpStream = $this->prophesize('Psr\Http\Message\StreamInterface');
+ $httpStream->getSize()->willReturn(0);
+ $httpStream->getMetadata('wrapper_data')->willReturn([
+ "Foo: bar",
+ "User-Agent: php",
+ "Content-Length: 1234",
+ "Asdf: qwer",
+ ]);
+
+ $stream = new ReadStream($httpStream->reveal());
+
+ $this->assertEquals(1234, $stream->getSize());
+ }
+
+ public function testNoContentLengthHeader()
+ {
+ $httpStream = $this->prophesize('Psr\Http\Message\StreamInterface');
+ $httpStream->getSize()->willReturn(null);
+ $httpStream->getMetadata('wrapper_data')->willReturn(array());
+
+ $stream = new ReadStream($httpStream->reveal());
+
+ $this->assertEquals(0, $stream->getSize());
+ }
+}
diff --git a/tests/unit/Storage/StorageClientTest.php b/tests/unit/Storage/StorageClientTest.php
index 97881840d082..8cbb5fff1304 100644
--- a/tests/unit/Storage/StorageClientTest.php
+++ b/tests/unit/Storage/StorageClientTest.php
@@ -15,9 +15,10 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Storage;
+namespace Google\Cloud\Tests\Unit\Storage;
use Google\Cloud\Storage\StorageClient;
+use Google\Cloud\Storage\StreamWrapper;
use Prophecy\Argument;
/**
@@ -82,6 +83,14 @@ public function testCreatesBucket()
$this->assertInstanceOf('Google\Cloud\Storage\Bucket', $this->client->createBucket('bucket'));
}
+
+ public function testRegisteringStreamWrapper()
+ {
+ $this->assertTrue($this->client->registerStreamWrapper());
+ $this->assertEquals($this->client, StreamWrapper::getClient());
+ $this->assertTrue(in_array('gs', stream_get_wrappers()));
+ $this->client->unregisterStreamWrapper();
+ }
}
class StorageTestClient extends StorageClient
diff --git a/tests/unit/Storage/StorageObjectTest.php b/tests/unit/Storage/StorageObjectTest.php
index af7d334b9ac5..4befeecb9486 100644
--- a/tests/unit/Storage/StorageObjectTest.php
+++ b/tests/unit/Storage/StorageObjectTest.php
@@ -15,9 +15,9 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Storage;
+namespace Google\Cloud\Tests\Unit\Storage;
-use Google\Cloud\Exception\NotFoundException;
+use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Storage\Connection\ConnectionInterface;
use Google\Cloud\Storage\Bucket;
use Google\Cloud\Storage\StorageObject;
@@ -71,15 +71,44 @@ public function testDelete()
public function testUpdatesData()
{
+ $object = 'object.txt';
$data = ['contentType' => 'image/jpg'];
- $this->connection->patchObject(Argument::any())->willReturn(['name' => 'object.txt'] + $data);
- $object = new StorageObject($this->connection->reveal(), 'object.txt', 'bucket', null, ['contentType' => 'image/png']);
+ $this->connection->patchObject(Argument::any())->willReturn(['name' => $object] + $data);
+ $object = new StorageObject(
+ $this->connection->reveal(),
+ $object,
+ 'bucket',
+ null,
+ ['contentType' => 'image/png']
+ );
$object->update($data);
$this->assertEquals($data['contentType'], $object->info()['contentType']);
}
+ public function testUpdatesDataAndUnsetsAclWithPredefinedAclApplied()
+ {
+ $object = 'object.txt';
+ $bucket = 'bucket';
+ $predefinedAcl = ['predefinedAcl' => 'private'];
+ $this->connection->patchObject($predefinedAcl + [
+ 'bucket' => $bucket,
+ 'object' => $object,
+ 'generation' => null,
+ 'acl' => null
+ ])->willReturn([]);
+ $object = new StorageObject(
+ $this->connection->reveal(),
+ $object,
+ $bucket,
+ null,
+ ['acl' => 'test']
+ );
+
+ $object->update([], $predefinedAcl);
+ }
+
public function testCopyObjectWithDefaultName()
{
$sourceBucket = 'bucket';
@@ -94,7 +123,7 @@ public function testCopyObjectWithDefaultName()
'destinationBucket' => $destinationBucket,
'destinationObject' => $objectName,
'destinationPredefinedAcl' => $acl,
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => [
'x-goog-encryption-algorithm' => 'AES256',
'x-goog-encryption-key' => $key,
@@ -176,7 +205,7 @@ public function testRewriteObjectWithDefaultName()
'destinationBucket' => $destinationBucket,
'destinationObject' => $objectName,
'destinationPredefinedAcl' => $acl,
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => [
'x-goog-copy-source-encryption-algorithm' => 'AES256',
'x-goog-copy-source-encryption-key' => $key,
@@ -280,7 +309,7 @@ public function testRenamesObject()
'destinationBucket' => $sourceBucket,
'destinationObject' => $newObjectName,
'destinationPredefinedAcl' => $acl,
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => [
'x-goog-encryption-algorithm' => 'AES256',
'x-goog-encryption-key' => $key,
@@ -319,7 +348,7 @@ public function testDownloadsAsString()
'bucket' => $bucket,
'object' => $object,
'generation' => null,
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => [
'x-goog-encryption-algorithm' => 'AES256',
'x-goog-encryption-key' => $key,
@@ -349,7 +378,7 @@ public function testDownloadsToFile()
'bucket' => $bucket,
'object' => $object,
'generation' => null,
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => [
'x-goog-encryption-algorithm' => 'AES256',
'x-goog-encryption-key' => $key,
@@ -400,7 +429,7 @@ public function testGetBodyWithExtraOptions()
'bucket' => $bucket,
'object' => $object,
'generation' => null,
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => [
'x-goog-encryption-algorithm' => 'AES256',
'x-goog-encryption-key' => $key,
@@ -450,7 +479,7 @@ public function testGetsInfoWithReload()
'bucket' => $bucket,
'object' => $object,
'generation' => null,
- 'httpOptions' => [
+ 'restOptions' => [
'headers' => [
'x-goog-encryption-algorithm' => 'AES256',
'x-goog-encryption-key' => $key,
@@ -482,4 +511,12 @@ public function testGetsIdentity()
$this->assertEquals($name, $object->identity()['object']);
$this->assertEquals($bucketName, $object->identity()['bucket']);
}
+
+ public function testGetsGcsUri()
+ {
+ $object = new StorageObject($this->connection->reveal(), $name = 'object.txt', $bucketName = 'bucket');
+
+ $expectedUri = sprintf('gs://%s/%s', $bucketName, $name);
+ $this->assertEquals($expectedUri, $object->gcsUri());
+ }
}
diff --git a/tests/unit/Storage/StreamWrapperTest.php b/tests/unit/Storage/StreamWrapperTest.php
new file mode 100644
index 000000000000..d65b6b40bcaf
--- /dev/null
+++ b/tests/unit/Storage/StreamWrapperTest.php
@@ -0,0 +1,426 @@
+client = $this->prophesize(StorageClient::class);
+ $this->bucket = $this->prophesize(Bucket::class);
+ $this->client->bucket('my_bucket')->willReturn($this->bucket->reveal());
+
+ StreamWrapper::register($this->client->reveal());
+ }
+
+ public function tearDown()
+ {
+ StreamWrapper::unregister();
+
+ parent::tearDown();
+ }
+
+ /**
+ * @group storageRead
+ */
+ public function testOpeningExistingFile()
+ {
+ $this->mockObjectData("existing_file.txt", "some data to read");
+
+ $fp = fopen('gs://my_bucket/existing_file.txt', 'r');
+ $this->assertEquals("some da", fread($fp, 7));
+ $this->assertEquals("ta to read", fread($fp, 1000));
+ fclose($fp);
+ }
+
+ /**
+ * @group storageRead
+ */
+ public function testOpeningNonExistentFileReturnsFalse()
+ {
+ $this->mockDownloadException('non-existent/file.txt', NotFoundException::class);
+
+ $fp = @fopen('gs://my_bucket/non-existent/file.txt', 'r');
+ $this->assertFalse($fp);
+ }
+
+ /**
+ * @group storageRead
+ */
+ public function testUnknownOpenMode()
+ {
+ $fp = @fopen('gs://my_bucket/existing_file.txt', 'a');
+ $this->assertFalse($fp);
+ }
+
+ /**
+ * @group storageRead
+ */
+ public function testFileGetContents()
+ {
+ $this->mockObjectData("file_get_contents.txt", "some data to read");
+
+ $this->assertEquals('some data to read', file_get_contents('gs://my_bucket/file_get_contents.txt'));
+ }
+
+ /**
+ * @group storageRead
+ */
+ public function testReadLines()
+ {
+ $this->mockObjectData("some_long_file.txt", "line1.\nline2.");
+
+ $fp = fopen('gs://my_bucket/some_long_file.txt', 'r');
+ $this->assertEquals("line1.\n", fgets($fp));
+ $this->assertEquals("line2.", fgets($fp));
+ fclose($fp);
+ }
+
+ /**
+ * @group storageWrite
+ */
+ public function testFileWrite()
+ {
+ $uploader = $this->prophesize(StreamableUploader::class);
+ $uploader->upload()->shouldBeCalled();
+ $uploader->getResumeUri()->willReturn('https://resume-uri/');
+ $this->bucket->getStreamableUploader("", Argument::type('array'))->willReturn($uploader->reveal());
+
+ $fp = fopen('gs://my_bucket/output.txt', 'w');
+ $this->assertEquals(6, fwrite($fp, "line1."));
+ $this->assertEquals(6, fwrite($fp, "line2."));
+ fclose($fp);
+ }
+
+ /**
+ * @group storageWrite
+ */
+ public function testFilePutContents()
+ {
+ $uploader = $this->prophesize(StreamableUploader::class);
+ $uploader->upload()->shouldBeCalled();
+ $uploader->getResumeUri()->willReturn('https://resume-uri/');
+ $this->bucket->getStreamableUploader("", Argument::type('array'))->willReturn($uploader->reveal());
+
+ file_put_contents('gs://my_bucket/file_put_contents.txt', 'Some data.');
+ }
+
+ /**
+ * @group storageSeek
+ */
+ public function testSeekOnWritableStream()
+ {
+ $uploader = $this->prophesize(StreamableUploader::class);
+ $this->bucket->getStreamableUploader("", Argument::type('array'))->willReturn($uploader->reveal());
+
+ $fp = fopen('gs://my_bucket/output.txt', 'w');
+ $this->assertEquals(-1, fseek($fp, 100));
+ fclose($fp);
+ }
+
+ /**
+ * @group storageSeek
+ */
+ public function testSeekOnReadableStream()
+ {
+ $this->mockObjectData("some_long_file.txt", "line1.\nline2.");
+ $fp = fopen('gs://my_bucket/some_long_file.txt', 'r');
+ $this->assertEquals(-1, fseek($fp, 100));
+ fclose($fp);
+ }
+
+ /**
+ * @group storageInfo
+ */
+ public function testFstat()
+ {
+ $this->mockObjectData("some_long_file.txt", "line1.\nline2.");
+ $fp = fopen('gs://my_bucket/some_long_file.txt', 'r');
+ $stat = fstat($fp);
+ $this->assertEquals(33206, $stat['mode']);
+ fclose($fp);
+ }
+
+ /**
+ * @group storageInfo
+ */
+ public function testStat()
+ {
+ $object = $this->prophesize(StorageObject::class);
+ $object->info()->willReturn([
+ 'size' => 1234,
+ 'updated' => '2017-01-19T19:31:35.833Z',
+ 'timeCreated' => '2017-01-19T19:31:35.833Z'
+ ]);
+ $this->bucket->object('some_long_file.txt')->willReturn($object->reveal());
+ $this->bucket->isWritable()->willReturn(true);
+
+ $stat = stat('gs://my_bucket/some_long_file.txt');
+ $this->assertEquals(33206, $stat['mode']);
+ }
+
+ /**
+ * @group storageInfo
+ * @expectedException PHPUnit_Framework_Error_Warning
+ */
+ public function testStatOnNonExistentFile()
+ {
+ $object = $this->prophesize(StorageObject::class);
+ $object->info()->willThrow(NotFoundException::class);
+ $this->bucket->object('non-existent/file.txt')->willReturn($object->reveal());
+
+ stat('gs://my_bucket/non-existent/file.txt');
+ }
+
+ /**
+ * @group storageDelete
+ */
+ public function testUnlink()
+ {
+ $obj = $this->prophesize(StorageObject::class);
+ $obj->delete()->willReturn(true)->shouldBeCalled();
+ $this->bucket->object('some_long_file.txt')->willReturn($obj->reveal());
+ $this->assertTrue(unlink('gs://my_bucket/some_long_file.txt'));
+ }
+
+ /**
+ * @group storageDelete
+ */
+ public function testUnlinkOnNonExistentFile()
+ {
+ $obj = $this->prophesize(StorageObject::class);
+ $obj->delete()->willThrow(NotFoundException::class);
+ $this->bucket->object('some_long_file.txt')->willReturn($obj->reveal());
+ $this->assertFalse(unlink('gs://my_bucket/some_long_file.txt'));
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testMkdir()
+ {
+ $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'publicRead'])->shouldBeCalled();
+ $this->assertTrue(mkdir('gs://my_bucket/foo/bar'));
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testMkdirProjectPrivate()
+ {
+ $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'projectPrivate'])->shouldBeCalled();
+ $this->assertTrue(mkdir('gs://my_bucket/foo/bar', 0740));
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testMkdirPrivate()
+ {
+ $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'private'])->shouldBeCalled();
+ $this->assertTrue(mkdir('gs://my_bucket/foo/bar', 0700));
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testMkdirOnBadDirectory()
+ {
+ $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'publicRead'])->willThrow(NotFoundException::class);
+ $this->assertFalse(mkdir('gs://my_bucket/foo/bar'));
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testMkDirCreatesBucket()
+ {
+ $this->bucket->exists()->willReturn(false);
+ $this->bucket->name()->willReturn('my_bucket');
+ $this->client->createBucket('my_bucket', [
+ 'predefinedAcl' => 'publicRead',
+ 'predefinedDefaultObjectAcl' => 'publicRead']
+ )->willReturn($this->bucket);
+ $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'publicRead'])->shouldBeCalled();
+
+ $this->assertTrue(mkdir('gs://my_bucket/foo/bar', 0777, STREAM_MKDIR_RECURSIVE));
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testRmdir()
+ {
+ $obj = $this->prophesize(StorageObject::class);
+ $obj->delete()->willReturn(true)->shouldBeCalled();
+ $this->bucket->object('foo/bar/')->willReturn($obj->reveal());
+ $this->assertTrue(rmdir('gs://my_bucket/foo/bar'));
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testRmdirOnBadDirectory()
+ {
+ $obj = $this->prophesize(StorageObject::class);
+ $obj->delete()->willThrow(NotFoundException::class);
+ $this->bucket->object('foo/bar/')->willReturn($obj->reveal());
+ $this->assertFalse(rmdir('gs://my_bucket/foo/bar'));
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testDirectoryListing()
+ {
+ $this->mockDirectoryListing('foo/', ['foo/file1.txt', 'foo/file2.txt', 'foo/file3.txt', 'foo/file4.txt']);
+ $fd = opendir('gs://my_bucket/foo/');
+ $this->assertEquals('foo/file1.txt', readdir($fd));
+ $this->assertEquals('foo/file2.txt', readdir($fd));
+ $this->assertEquals('foo/file3.txt', readdir($fd));
+ rewinddir($fd);
+ $this->assertEquals('foo/file1.txt', readdir($fd));
+ closedir($fd);
+ }
+
+ /**
+ * @group storageDirectory
+ */
+ public function testDirectoryListingViaScan()
+ {
+ $files = ['foo/file1.txt', 'foo/file2.txt', 'foo/file3.txt', 'foo/file4.txt'];
+ $this->mockDirectoryListing('foo/', $files);
+ $this->assertEquals($files, scandir('gs://my_bucket/foo/'));
+ }
+
+ public function testRenameFile()
+ {
+ $this->mockDirectoryListing('foo.txt', ['foo.txt']);
+ $object = $this->prophesize(StorageObject::class);
+ $object->rename('new_location/foo.txt', ['destinationBucket' => 'my_bucket'])->shouldBeCalled();
+ $this->bucket->object('foo.txt')->willReturn($object->reveal());
+
+ $this->assertTrue(rename('gs://my_bucket/foo.txt', 'gs://my_bucket/new_location/foo.txt'));
+ }
+
+ public function testRenameToDifferentBucket()
+ {
+ $this->mockDirectoryListing('foo.txt', ['foo.txt']);
+ $object = $this->prophesize(StorageObject::class);
+ $object->rename('bar/foo.txt', ['destinationBucket' => 'another_bucket'])->shouldBeCalled();
+ $this->bucket->object('foo.txt')->willReturn($object->reveal());
+
+ $this->assertTrue(rename('gs://my_bucket/foo.txt', 'gs://another_bucket/bar/foo.txt'));
+ }
+
+ public function testRenameDirectory()
+ {
+ $this->mockDirectoryListing('foo', ['foo/bar1.txt', 'foo/bar2.txt', 'foo/asdf/bar.txt']);
+
+ $object = $this->prophesize(StorageObject::class);
+ $object->rename('nested/folder/bar1.txt', ['destinationBucket' => 'another_bucket'])->shouldBeCalled();
+ $this->bucket->object('foo/bar1.txt')->willReturn($object->reveal());
+
+ $object = $this->prophesize(StorageObject::class);
+ $object->rename('nested/folder/bar2.txt', ['destinationBucket' => 'another_bucket'])->shouldBeCalled();
+ $this->bucket->object('foo/bar2.txt')->willReturn($object->reveal());
+
+ $object = $this->prophesize(StorageObject::class);
+ $object->rename('nested/folder/asdf/bar.txt', ['destinationBucket' => 'another_bucket'])->shouldBeCalled();
+ $this->bucket->object('foo/asdf/bar.txt')->willReturn($object->reveal());
+
+ $this->assertTrue(rename('gs://my_bucket/foo', 'gs://another_bucket/nested/folder'));
+ }
+
+ public function testCanSpecifyChunkSizeViaContext()
+ {
+
+ $uploader = $this->prophesize(StreamableUploader::class);
+ $upload = $uploader->upload(5)->willReturn(array())->shouldBeCalled();
+ $uploader->upload()->shouldBeCalled();
+ $uploader->getResumeUri()->willReturn('https://resume-uri/');
+ $this->bucket->getStreamableUploader("", Argument::type('array'))->willReturn($uploader->reveal());
+
+ $context = stream_context_create(array(
+ 'gs' => array(
+ 'chunkSize' => 5
+ )
+ ));
+ $fp = fopen('gs://my_bucket/existing_file.txt', 'w', false, $context);
+ $this->assertEquals(9, fwrite($fp, "123456789"));
+ fclose($fp);
+ }
+
+ private function mockObjectData($file, $data, $bucket = null)
+ {
+ $bucket = $bucket ?: $this->bucket;
+ $stream = new \GuzzleHttp\Psr7\BufferStream(100);
+ $stream->write($data);
+ $object = $this->prophesize(StorageObject::class);
+ $object->downloadAsStream(Argument::any())->willReturn($stream);
+ $bucket->object($file)->willReturn($object->reveal());
+ }
+
+ private function mockDownloadException($file, $exception)
+ {
+ $object = $this->prophesize(StorageObject::class);
+ $object->downloadAsStream(Argument::any())->willThrow($exception);
+ $this->bucket->object($file)->willReturn($object->reveal());
+ }
+
+ private function mockDirectoryListing($path, $filesToReturn)
+ {
+ $test = $this;
+ $this->bucket->objects(
+ Argument::that(function($options) use ($path) {
+ return $options['prefix'] == $path;
+ })
+ )->will(function() use ($test, $filesToReturn) {
+ return $test->fileListGenerator($filesToReturn);
+ });
+ }
+
+ private function fileListGenerator($fileToReturn)
+ {
+ foreach($fileToReturn as $file) {
+ $obj = $this->prophesize(StorageObject::class);
+ $obj->name()->willReturn($file);
+ yield $obj->reveal();
+ }
+ }
+}
diff --git a/tests/unit/Storage/WriteStreamTest.php b/tests/unit/Storage/WriteStreamTest.php
new file mode 100644
index 000000000000..505d584dffa9
--- /dev/null
+++ b/tests/unit/Storage/WriteStreamTest.php
@@ -0,0 +1,56 @@
+prophesize(StreamableUploader::class);
+ $uploader->getResumeUri()->willReturn('https://some-resume-uri/');
+ $stream = new WriteStream($uploader->reveal(), ['chunkSize' => 10]);
+
+ // We should see 2 calls to upload with size of 10.
+ $upload = $uploader->upload(10)->will(function($args) use ($stream) {
+ if (count($args) > 0) {
+ $size = $args[0];
+ $stream->read(10);
+ }
+ return array();
+ });
+
+ // We should see a single call to finish the upload.
+ $uploader->upload()->shouldBeCalledTimes(1);
+
+ $stream->write('1234567');
+ $upload->shouldHaveBeenCalledTimes(0);
+ $stream->write('8901234');
+ $upload->shouldHaveBeenCalledTimes(1);
+ $stream->write('5678901');
+ $upload->shouldHaveBeenCalledTimes(2);
+ $stream->close();
+ }
+}
diff --git a/tests/unit/Translate/Connection/RestTest.php b/tests/unit/Translate/Connection/RestTest.php
index 173733bc5628..de2431cae741 100644
--- a/tests/unit/Translate/Connection/RestTest.php
+++ b/tests/unit/Translate/Connection/RestTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Translate\Connection;
+namespace Google\Cloud\Tests\Unit\Translate\Connection;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\Translate\Connection\Rest;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
+use Google\Cloud\Translate\TranslateClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
diff --git a/tests/unit/Translate/TranslateClientTest.php b/tests/unit/Translate/TranslateClientTest.php
index 57efdcf25e01..1cc2dc15edc2 100644
--- a/tests/unit/Translate/TranslateClientTest.php
+++ b/tests/unit/Translate/TranslateClientTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Translate;
+namespace Google\Cloud\Tests\Unit\Translate;
use Google\Cloud\Translate\Connection\ConnectionInterface;
use Google\Cloud\Translate\TranslateClient;
@@ -41,7 +41,7 @@ public function testWithNoKey()
$client = new TranslateTestClient();
$this->connection->listTranslations(Argument::that(function($args) {
- if (!is_null($args['key'])) {
+ if (isset($args['key'])) {
return false;
}
@@ -53,6 +53,24 @@ public function testWithNoKey()
$client->translate('foo');
}
+ public function testTranslateModel()
+ {
+ $this->connection->listTranslations(Argument::that(function ($args) {
+ if (isset($args['model'])) return false;
+ }));
+
+ $this->client->setConnection($this->connection->reveal());
+
+ $this->client->translate('foo bar');
+
+ $this->connection->listTranslations(Argument::that(function ($args) {
+ if ($args['model'] !== 'base') return false;
+ }));
+
+ $this->client->setConnection($this->connection->reveal());
+ $this->client->translate('foo bar', ['model' => 'base']);
+ }
+
public function testTranslate()
{
$expected = $this->getTranslateExpectedData('translate', 'translated', 'en');
diff --git a/tests/unit/Vision/Annotation/CropHintTest.php b/tests/unit/Vision/Annotation/CropHintTest.php
new file mode 100644
index 000000000000..971a9198a628
--- /dev/null
+++ b/tests/unit/Vision/Annotation/CropHintTest.php
@@ -0,0 +1,55 @@
+info = [
+ 'boundingPoly' => ['foo' => 'bar'],
+ 'confidence' => 0.4,
+ 'importanceFraction' => 0.1
+ ];
+
+ $this->hint = new CropHint($this->info);
+ }
+
+ public function testBoundingPoly()
+ {
+ $this->assertEquals($this->info['boundingPoly'], $this->hint->boundingPoly());
+ }
+
+ public function testConfidence()
+ {
+ $this->assertEquals($this->info['confidence'], $this->hint->confidence());
+ }
+
+ public function testImportanceFraction()
+ {
+ $this->assertEquals($this->info['importanceFraction'], $this->hint->importanceFraction());
+ }
+}
diff --git a/tests/unit/Vision/Annotation/DocumentTest.php b/tests/unit/Vision/Annotation/DocumentTest.php
new file mode 100644
index 000000000000..6e6ece9658c4
--- /dev/null
+++ b/tests/unit/Vision/Annotation/DocumentTest.php
@@ -0,0 +1,38 @@
+ 'bar'
+ ];
+
+ $e = new Document($res);
+
+ $this->assertEquals($res, $e->info());
+ $this->assertEquals('bar', $e->foo());
+ }
+}
diff --git a/tests/unit/Vision/Annotation/EntityTest.php b/tests/unit/Vision/Annotation/EntityTest.php
index 5173671f46dc..3a872891ae8d 100644
--- a/tests/unit/Vision/Annotation/EntityTest.php
+++ b/tests/unit/Vision/Annotation/EntityTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision\Annotation;
+namespace Google\Cloud\Tests\Unit\Vision\Annotation;
use Google\Cloud\Vision\Annotation\Entity;
diff --git a/tests/unit/Vision/Annotation/Face/LandmarksTest.php b/tests/unit/Vision/Annotation/Face/LandmarksTest.php
index 46875e3ffb05..531acfbdf6f3 100644
--- a/tests/unit/Vision/Annotation/Face/LandmarksTest.php
+++ b/tests/unit/Vision/Annotation/Face/LandmarksTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision\Annotation;
+namespace Google\Cloud\Tests\Unit\Vision\Annotation;
use Google\Cloud\Vision\Annotation\Face\Landmarks;
diff --git a/tests/unit/Vision/Annotation/FaceTest.php b/tests/unit/Vision/Annotation/FaceTest.php
index da91c700eef5..883203f80408 100644
--- a/tests/unit/Vision/Annotation/FaceTest.php
+++ b/tests/unit/Vision/Annotation/FaceTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision\Annotation;
+namespace Google\Cloud\Tests\Unit\Vision\Annotation;
use Google\Cloud\Vision\Annotation\Face;
use Google\Cloud\Vision\Annotation\Face\Landmarks;
diff --git a/tests/unit/Vision/Annotation/LikelihoodTraitTest.php b/tests/unit/Vision/Annotation/LikelihoodTraitTest.php
index dbeccb5a32c1..706b5136c554 100644
--- a/tests/unit/Vision/Annotation/LikelihoodTraitTest.php
+++ b/tests/unit/Vision/Annotation/LikelihoodTraitTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision\Annotation;
+namespace Google\Cloud\Tests\Unit\Vision\Annotation;
use Google\Cloud\Vision\Annotation\FeatureInterface;
use Google\Cloud\Vision\Annotation\LikelihoodTrait;
diff --git a/tests/unit/Vision/Annotation/SafeSearchTest.php b/tests/unit/Vision/Annotation/SafeSearchTest.php
index afb877aea58c..6ae2a08c5b20 100644
--- a/tests/unit/Vision/Annotation/SafeSearchTest.php
+++ b/tests/unit/Vision/Annotation/SafeSearchTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision\Annotation;
+namespace Google\Cloud\Tests\Unit\Vision\Annotation;
use Google\Cloud\Vision\Annotation\SafeSearch;
diff --git a/tests/unit/Vision/Annotation/Web/WebEntityTest.php b/tests/unit/Vision/Annotation/Web/WebEntityTest.php
new file mode 100644
index 000000000000..2ea3269ea43a
--- /dev/null
+++ b/tests/unit/Vision/Annotation/Web/WebEntityTest.php
@@ -0,0 +1,54 @@
+info = [
+ 'entityId' => 'foo',
+ 'score' => 1,
+ 'description' => 'bar'
+ ];
+ $this->entity = new WebEntity($this->info);
+ }
+
+ public function testEntityId()
+ {
+ $this->assertEquals($this->info['entityId'], $this->entity->entityId());
+ }
+
+ public function testScore()
+ {
+ $this->assertEquals($this->info['score'], $this->entity->score());
+ }
+
+ public function testDescription()
+ {
+ $this->assertEquals($this->info['description'], $this->entity->description());
+ }
+}
diff --git a/tests/unit/Vision/Annotation/Web/WebImageTest.php b/tests/unit/Vision/Annotation/Web/WebImageTest.php
new file mode 100644
index 000000000000..f5da1e1de743
--- /dev/null
+++ b/tests/unit/Vision/Annotation/Web/WebImageTest.php
@@ -0,0 +1,48 @@
+info = [
+ 'url' => 'http://foo.bar/bat',
+ 'score' => 0.4
+ ];
+ $this->image = new WebImage($this->info);
+ }
+
+ public function testUrl()
+ {
+ $this->assertEquals($this->info['url'], $this->image->url());
+ }
+
+ public function testScore()
+ {
+ $this->assertEquals($this->info['score'], $this->image->score());
+ }
+}
diff --git a/tests/unit/Vision/Annotation/Web/WebPageTest.php b/tests/unit/Vision/Annotation/Web/WebPageTest.php
new file mode 100644
index 000000000000..488ad080ec8c
--- /dev/null
+++ b/tests/unit/Vision/Annotation/Web/WebPageTest.php
@@ -0,0 +1,48 @@
+info = [
+ 'url' => 'http://foo.bar/bat',
+ 'score' => 0.4
+ ];
+ $this->image = new WebPage($this->info);
+ }
+
+ public function testUrl()
+ {
+ $this->assertEquals($this->info['url'], $this->image->url());
+ }
+
+ public function testScore()
+ {
+ $this->assertEquals($this->info['score'], $this->image->score());
+ }
+}
diff --git a/tests/unit/Vision/Annotation/WebTest.php b/tests/unit/Vision/Annotation/WebTest.php
new file mode 100644
index 000000000000..1b449043471f
--- /dev/null
+++ b/tests/unit/Vision/Annotation/WebTest.php
@@ -0,0 +1,75 @@
+info = [
+ 'webEntities' => [
+ ['foo' => 'bar']
+ ],
+ 'fullMatchingImages' => [
+ ['foo' => 'bar']
+ ],
+ 'partialMatchingImages' => [
+ ['foo' => 'bar']
+ ],
+ 'pagesWithMatchingImages' => [
+ ['foo' => 'bar']
+ ]
+ ];
+ $this->annotation = new Web($this->info);
+ }
+
+ public function testEntities()
+ {
+ $this->assertInstanceOf(WebEntity::class, $this->annotation->entities()[0]);
+ $this->assertEquals($this->info['webEntities'][0], $this->annotation->entities()[0]->info());
+ }
+
+ public function testMatchingImages()
+ {
+ $this->assertInstanceOf(WebImage::class, $this->annotation->matchingImages()[0]);
+ $this->assertEquals($this->info['fullMatchingImages'][0], $this->annotation->matchingImages()[0]->info());
+ }
+
+ public function testPartialMatchingImages()
+ {
+ $this->assertInstanceOf(WebImage::class, $this->annotation->partialMatchingImages()[0]);
+ $this->assertEquals($this->info['partialMatchingImages'][0], $this->annotation->partialMatchingImages()[0]->info());
+ }
+
+ public function testPages()
+ {
+ $this->assertInstanceOf(WebPage::class, $this->annotation->pages()[0]);
+ $this->assertEquals($this->info['pagesWithMatchingImages'][0], $this->annotation->pages()[0]->info());
+ }
+}
diff --git a/tests/unit/Vision/AnnotationTest.php b/tests/unit/Vision/AnnotationTest.php
index 746b2f003e30..0e2745536c17 100644
--- a/tests/unit/Vision/AnnotationTest.php
+++ b/tests/unit/Vision/AnnotationTest.php
@@ -15,13 +15,16 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision;
+namespace Google\Cloud\Tests\Unit\Vision;
use Google\Cloud\Vision\Annotation;
+use Google\Cloud\Vision\Annotation\CropHint;
+use Google\Cloud\Vision\Annotation\Document;
use Google\Cloud\Vision\Annotation\Entity;
use Google\Cloud\Vision\Annotation\Face;
use Google\Cloud\Vision\Annotation\ImageProperties;
use Google\Cloud\Vision\Annotation\SafeSearch;
+use Google\Cloud\Vision\Annotation\Web;
/**
* @group vision
@@ -40,7 +43,10 @@ public function testConstruct()
'textAnnotations' => ['foo' => ['bat' => 'bar']],
'safeSearchAnnotation' => ['foo' => ['bat' => 'bar']],
'imagePropertiesAnnotation' => ['foo' => ['bat' => 'bar']],
- 'error' => ['foo' => ['bat' => 'bar']]
+ 'error' => ['foo' => ['bat' => 'bar']],
+ 'fullTextAnnotation' => ['foo' => 'bar'],
+ 'cropHintsAnnotation' => ['cropHints' => [['bat' => 'bar']]],
+ 'webDetection' => ['foo' => ['bat' => 'bar']],
];
$ann = new Annotation($res);
@@ -53,6 +59,9 @@ public function testConstruct()
$this->assertInstanceOf(SafeSearch::class, $ann->safeSearch());
$this->assertInstanceOf(ImageProperties::class, $ann->imageProperties());
$this->assertEquals($res['error'], $ann->error());
+ $this->assertInstanceOf(Document::class, $ann->fullText());
+ $this->assertInstanceOf(CropHint::class, $ann->cropHints()[0]);
+ $this->assertInstanceOf(Web::class, $ann->web());
$this->assertEquals($res, $ann->info());
}
diff --git a/tests/unit/Vision/Connection/RestTest.php b/tests/unit/Vision/Connection/RestTest.php
index e774d512823f..7fdbc2ee27b9 100644
--- a/tests/unit/Vision/Connection/RestTest.php
+++ b/tests/unit/Vision/Connection/RestTest.php
@@ -15,11 +15,12 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision\Connection;
+namespace Google\Cloud\Tests\Unit\Vision\Connection;
+use Google\Cloud\Core\RequestBuilder;
+use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\Vision\Connection\Rest;
-use Google\Cloud\RequestBuilder;
-use Google\Cloud\RequestWrapper;
+use Google\Cloud\Vision\VisionClient;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
diff --git a/tests/unit/Vision/ImageTest.php b/tests/unit/Vision/ImageTest.php
index 7ba69af573a8..4dac191fc94b 100644
--- a/tests/unit/Vision/ImageTest.php
+++ b/tests/unit/Vision/ImageTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision;
+namespace Google\Cloud\Tests\Unit\Vision;
use Google\Cloud\Storage\StorageClient;
use Google\Cloud\Vision\Image;
@@ -25,7 +25,7 @@
*/
class ImageTest extends \PHPUnit_Framework_TestCase
{
- public function testWithBytes()
+ public function testWithString()
{
$bytes = file_get_contents(__DIR__ .'/../fixtures/vision/eiffel-tower.jpg');
$image = new Image($bytes, ['landmarks']);
@@ -47,7 +47,7 @@ public function testWithStorage()
$image = new Image($object, [ 'landmarks' ]);
$res = $image->requestObject();
- $this->assertEquals($res['image']['source']['gcsImageUri'], $gcsUri);
+ $this->assertEquals($res['image']['source']['imageUri'], $gcsUri);
$this->assertEquals($res['features'], [ ['type' => 'LANDMARK_DETECTION'] ]);
}
@@ -63,6 +63,17 @@ public function testWithResource()
$this->assertEquals($res['features'], [ ['type' => 'LANDMARK_DETECTION'] ]);
}
+ public function testWithExternalImage()
+ {
+ $externalUri = 'http://google.com/image.jpg';
+ $image = new Image($externalUri, ['landmarks']);
+
+ $res = $image->requestObject();
+
+ $this->assertEquals($res['image']['source']['imageUri'], $externalUri);
+ $this->assertEquals($res['features'], [ ['type' => 'LANDMARK_DETECTION'] ]);
+ }
+
/**
* @expectedException InvalidArgumentException
*/
@@ -87,13 +98,16 @@ public function testMaxResults()
public function testShortNamesMapping()
{
$names = [
- 'faces' => 'FACE_DETECTION',
- 'landmarks' => 'LANDMARK_DETECTION',
- 'logos' => 'LOGO_DETECTION',
- 'labels' => 'LABEL_DETECTION',
- 'text' => 'TEXT_DETECTION',
- 'safeSearch' => 'SAFE_SEARCH_DETECTION',
- 'imageProperties' => 'IMAGE_PROPERTIES'
+ 'faces' => 'FACE_DETECTION',
+ 'landmarks' => 'LANDMARK_DETECTION',
+ 'logos' => 'LOGO_DETECTION',
+ 'labels' => 'LABEL_DETECTION',
+ 'text' => 'TEXT_DETECTION',
+ 'document' => 'DOCUMENT_TEXT_DETECTION',
+ 'safeSearch' => 'SAFE_SEARCH_DETECTION',
+ 'imageProperties' => 'IMAGE_PROPERTIES',
+ 'crop' => 'CROP_HINTS',
+ 'web' => 'WEB_DETECTION'
];
$bytes = 'foo';
@@ -122,4 +136,26 @@ public function testBytesWithoutEncoding()
$encodedRes = $image->requestObject();
$this->assertEquals($encodedRes['image']['content'], base64_encode($bytes));
}
+
+ public function testUrlSchemes()
+ {
+ $urls = [
+ 'http://foo.bar',
+ 'https://foo.bar',
+ 'gs://foo/bar',
+ 'ssh://foo/bar'
+ ];
+
+ $images = [
+ new Image($urls[0], ['faces']),
+ new Image($urls[1], ['faces']),
+ new Image($urls[2], ['faces']),
+ new Image($urls[3], ['faces']),
+ ];
+
+ $this->assertEquals($urls[0], $images[0]->requestObject()['image']['source']['imageUri']);
+ $this->assertEquals($urls[1], $images[1]->requestObject()['image']['source']['imageUri']);
+ $this->assertEquals($urls[2], $images[2]->requestObject()['image']['source']['imageUri']);
+ $this->assertFalse(isset($images[3]->requestObject()['image']['source']['imageUri']));
+ }
}
diff --git a/tests/unit/Vision/VisionClientTest.php b/tests/unit/Vision/VisionClientTest.php
index 66cb42d25ae4..12ed2e548d25 100644
--- a/tests/unit/Vision/VisionClientTest.php
+++ b/tests/unit/Vision/VisionClientTest.php
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-namespace Google\Cloud\Tests\Vision;
+namespace Google\Cloud\Tests\Unit\Vision;
use Google\Cloud\Vision\Annotation;
use Google\Cloud\Vision\Connection\ConnectionInterface;
diff --git a/tests/unit/fixtures/spanner/instance.json b/tests/unit/fixtures/spanner/instance.json
new file mode 100644
index 000000000000..fcf371769ce3
--- /dev/null
+++ b/tests/unit/fixtures/spanner/instance.json
@@ -0,0 +1,7 @@
+{
+ "name": "projects\/test-project\/instances\/instance-name",
+ "config": "projects\/test-project\/instanceConfigs\/regional-europe-west1",
+ "displayName": "Instance Name",
+ "nodeCount": 1,
+ "state": 2
+}