allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

View File

@@ -0,0 +1,98 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\UpdateMetadataInterface;
/**
* Provides an anonymous set of credentials, which is useful for APIs which do
* not require authentication.
*/
class AnonymousCredentials implements
FetchAuthTokenInterface,
UpdateMetadataInterface,
GetQuotaProjectInterface
{
/**
* @var array
*/
private $token = [
'access_token' => null
];
/**
* Fetches the auth token. In this case it returns a null value.
*
* @param callable $httpHandler
* @return array
*/
public function fetchAuthToken(?callable $httpHandler = null)
{
return $this->token;
}
/**
* Returns the cache key. In this case it returns a null value, disabling
* caching.
*
* @return string|null
*/
public function getCacheKey()
{
return null;
}
/**
* Fetches the last received token. In this case, it returns the same null
* auth token.
*
* @return array
*/
public function getLastReceivedToken()
{
return $this->token;
}
/**
* This method has no effect for AnonymousCredentials.
*
* @param array $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable $httpHandler callback which delivers psr7 request
* @return array updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
?callable $httpHandler = null
) {
return $metadata;
}
/**
* This method always returns null for AnonymousCredentials.
*
* @return string|null
*/
public function getQuotaProject()
{
return null;
}
}

View File

@@ -0,0 +1,290 @@
<?php
/**
* Copyright 2023 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\ApiCore\ArrayTrait;
use Google\ApiCore\Options\CallOptions;
use Google\Protobuf\Internal\Message;
use Google\Protobuf\NullValue;
use LogicException;
/**
* @internal
* Supplies helper methods to interact with the APIs.
*/
trait ApiHelperTrait
{
use ArrayTrait;
use TimeTrait;
private OptionsValidator $optionsValidator;
/**
* Format a struct for the API.
*
* @param array $fields
* @return array
*/
private function formatStructForApi(array $fields)
{
$fFields = [];
foreach ($fields as $key => $value) {
$fFields[$key] = $this->formatValueForApi($value);
}
return ['fields' => $fFields];
}
private function unpackStructFromApi(array $struct)
{
$vals = [];
foreach ($struct['fields'] as $key => $val) {
$vals[$key] = $this->unpackValue($val);
}
return $vals;
}
private function unpackValue($value)
{
if (count($value) > 1) {
throw new \RuntimeException("Unexpected fields in struct: $value");
}
foreach ($value as $setField => $setValue) {
switch ($setField) {
case 'listValue':
$valueList = [];
foreach ($setValue['values'] as $innerValue) {
$valueList[] = $this->unpackValue($innerValue);
}
return $valueList;
case 'structValue':
return $this->unpackStructFromApi($value['structValue']);
default:
return $setValue;
}
}
}
private function flattenStruct(array $struct)
{
return $struct['fields'];
}
private function flattenValue(array $value)
{
if (count($value) > 1) {
throw new \RuntimeException("Unexpected fields in struct: $value");
}
if (isset($value['nullValue'])) {
return null;
}
return array_pop($value);
}
private function flattenListValue(array $value)
{
return $value['values'];
}
/**
* Format a list for the API.
*
* @param array $list
* @return array
*/
private function formatListForApi(array $list)
{
$values = [];
foreach ($list as $value) {
$values[] = $this->formatValueForApi($value);
}
return ['values' => $values];
}
/**
* Format a value for the API.
*
* @param mixed $value
* @return array
*/
private function formatValueForApi($value)
{
$type = gettype($value);
switch ($type) {
case 'string':
return ['string_value' => $value];
case 'double':
case 'integer':
return ['number_value' => $value];
case 'boolean':
return ['bool_value' => $value];
case 'NULL':
return ['null_value' => NullValue::NULL_VALUE];
case 'array':
if (!empty($value) && $this->isAssoc($value)) {
return ['struct_value' => $this->formatStructForApi($value)];
}
return ['list_value' => $this->formatListForApi($value)];
}
return [];
}
/**
* Format a gRPC timestamp to match the format returned by the REST API.
*
* @param array $timestamp
* @return string
*/
private function formatTimestampFromApi(array $timestamp)
{
$timestamp += [
'seconds' => 0,
'nanos' => 0
];
$dt = $this->createDateTimeFromSeconds($timestamp['seconds']);
return $this->formatTimeAsString($dt, $timestamp['nanos']);
}
/**
* Format a timestamp for the API with nanosecond precision.
*
* @param string $value
* @return array
*/
private function formatTimestampForApi($value)
{
list($dt, $nanos) = $this->parseTimeString($value);
return [
'seconds' => (int) $dt->format('U'),
'nanos' => (int) $nanos
];
}
/**
* Format a duration for the API.
*
* @param string|mixed $value
* @return array
*/
private function formatDurationForApi($value)
{
if (is_string($value)) {
$d = explode('.', trim($value, 's'));
if (count($d) < 2) {
$seconds = $d[0];
$nanos = 0;
} else {
$seconds = (int) $d[0];
$nanos = $this->convertFractionToNanoSeconds($d[1]);
}
} elseif ($value instanceof Duration) {
$d = $value->get();
$seconds = $d['seconds'];
$nanos = $d['nanos'];
}
return [
'seconds' => $seconds,
'nanos' => $nanos
];
}
/**
* Construct a gapic client. Allows for tests to intercept.
*
* @param string $gapicName
* @param array $config
* @return mixed
*/
protected function constructGapic($gapicName, array $config)
{
return new $gapicName($config);
}
/**
* Helper function to convert selective elements into protos out of a given input array.
*
* Example:
* ```
* $output = $topic->convertDataToProtos(['schema' =>[], 'other vals'], ['schema' => Schema::class]);
* $output['schema']; // This will be of the Schema type.
* ```
*
* @param array $input The input array.
* @param array $map The key,value pairs specifying the elements and the proto classes.
*
* @return array The modified array
*/
private function convertDataToProtos(array $input, array $map): array
{
if (!isset($this->serializer)) {
throw new \LogicException('Serializer must be set to use this function');
}
foreach ($map as $key => $className) {
if (isset($input[$key])) {
$input[$key] = $this->serializer->decodeMessage(new $className(), $input[$key]);
}
}
return $input;
}
/**
* Helper method used to split a supplied set of options into parameters that are passed into
* a proto message and optional args.
* We strictly treat the parameters allowed by `CallOptions` in GAX as the optional params
* and everything else that is passed is passed to the Proto message constructor.
*/
private function splitOptionalArgs(array $input, array $extraAllowedKeys = []): array
{
$callOptionFields = array_keys((new CallOptions([]))->toArray());
$keys = array_merge($callOptionFields, $extraAllowedKeys);
$callOptions = $this->pluckArray($keys, $input);
return [$input, $callOptions];
}
/**
* Helper method used to validate optons based on the supplied $optionTypes
* $optionTypes can be an array of string keys, a protobuf Message classname, or a
* the CallOptions classname. Parameters are split and returned in the order
* that the options types are provided.
*
* @throws LogicException
*/
private function validateOptions(array $options, array|Message|string ...$optionTypes): array
{
if (!isset($this->optionsValidator)) {
$this->optionsValidator = new OptionsValidator();
}
return $this->optionsValidator->validateOptions($options, ...$optionTypes);
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Provides basic array helper methods.
*/
trait ArrayTrait
{
/**
* Pluck a value out of an array.
*
* @param string $key
* @param array $arr
* @param bool $isRequired
* @return mixed
* @throws \InvalidArgumentException
*/
private function pluck($key, array &$arr, $isRequired = true)
{
if (!array_key_exists($key, $arr)) {
if ($isRequired) {
throw new \InvalidArgumentException(
"Key $key does not exist in the provided array."
);
}
return null;
}
$value = $arr[$key];
unset($arr[$key]);
return $value;
}
/**
* Pluck a subset of an array.
*
* @param string[] $keys
* @param array $arr
* @return array
*/
private function pluckArray(array $keys, &$arr)
{
$values = [];
foreach ($keys as $key) {
if (array_key_exists($key, $arr)) {
$values[$key] = $this->pluck($key, $arr, false);
}
}
return $values;
}
/**
* Determine whether given array is associative.
* If $arr is empty, then $onEmpty will be returned
* $onEmpty defaults to true to maintain compatibility
* with the current usage.
*
* @param array $arr
* @param bool $onEmpty
* @return bool
*/
private function isAssoc(array $arr, $onEmpty = true)
{
if (empty($arr)) {
return $onEmpty;
}
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) {
return !is_null($element);
});
}
/**
* A method, similar to PHP's `array_merge_recursive`, with two differences.
*
* 1. Keys in $array2 take precedence over keys in $array1.
* 2. Non-array keys found in both inputs are not transformed into an array
* and appended. Rather, the value in $array2 is used.
*
* @param array $array1
* @param array $array2
* @return array
*/
private function arrayMergeRecursive(array $array1, array $array2)
{
foreach ($array2 as $key => $value) {
if (array_key_exists($key, $array1) && is_array($array1[$key]) && is_array($value)) {
$array1[$key] = ($this->isAssoc($array1[$key]) && $this->isAssoc($value))
? $this->arrayMergeRecursive($array1[$key], $value)
: array_merge($array1[$key], $value);
} else {
$array1[$key] = $value;
}
}
return $array1;
}
}

View File

@@ -0,0 +1,162 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use Google\Cloud\Core\SysvTrait;
/**
* An external daemon script for executing the batch jobs.
*
* @codeCoverageIgnore
*
* The system test is responsible for testing this class.
* {@see \Google\Cloud\Tests\System\Core\Batch}
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class BatchDaemon
{
use BatchDaemonTrait;
use HandleFailureTrait;
use SysvTrait;
use InterruptTrait;
/* @var BatchRunner */
private $runner;
/* @var array */
private $descriptorSpec;
/* @var string */
private $command;
/**
* Prepare the descriptor spec and install signal handlers.
*
* @param string $entrypoint Daemon's entrypoint script.
* @throws \RuntimeException
*/
public function __construct($entrypoint)
{
if (! $this->isSysvIPCLoaded()) {
throw new \RuntimeException('SystemV IPC extensions are missing.');
}
$this->runner = new BatchRunner(
new SysvConfigStorage(),
new SysvProcessor()
);
$this->shutdown = false;
// Just share the usual descriptors.
$this->descriptorSpec = [
0 => ['file', 'php://stdin', 'r'],
1 => ['file', 'php://stdout', 'w'],
2 => ['file', 'php://stderr', 'w']
];
$this->command = sprintf('exec php -d auto_prepend_file="" %s daemon', $entrypoint);
$this->initFailureFile();
}
/**
* A loop for the parent.
*
* @return void
*/
public function run()
{
$this->setupSignalHandlers();
$procs = [];
while (true) {
$jobs = $this->runner->getJobs();
foreach ($jobs as $job) {
if (! array_key_exists($job->identifier(), $procs)) {
$procs[$job->identifier()] = [];
}
while (count($procs[$job->identifier()]) > $job->numWorkers()) {
// Stopping an excessive child.
echo 'Stopping an excessive child.' . PHP_EOL;
$proc = array_pop($procs[$job->identifier()]);
$status = proc_get_status($proc);
// Keep sending SIGTERM until the child exits.
while ($status['running'] === true) {
@proc_terminate($proc);
usleep(50000);
$status = proc_get_status($proc);
}
@proc_close($proc);
}
for ($i = 0; $i < $job->numWorkers(); $i++) {
$needStart = false;
if (array_key_exists($i, $procs[$job->identifier()])) {
$status = proc_get_status($procs[$job->identifier()][$i]);
if ($status['running'] !== true) {
$needStart = true;
}
} else {
$needStart = true;
}
if ($needStart) {
echo 'Starting a child.' . PHP_EOL;
$procs[$job->identifier()][$i] = proc_open(
sprintf('%s %d', $this->command, $job->id()),
$this->descriptorSpec,
$pipes
);
}
}
}
usleep(1000000); // Reload the config after 1 second
pcntl_signal_dispatch();
if ($this->shutdown) {
echo 'Shutting down, waiting for the children' . PHP_EOL;
foreach ($procs as $k => $v) {
foreach ($v as $proc) {
$status = proc_get_status($proc);
// Keep sending SIGTERM until the child exits.
while ($status['running'] === true) {
@proc_terminate($proc);
usleep(50000);
$status = proc_get_status($proc);
}
@proc_close($proc);
}
}
echo 'BatchDaemon exiting' . PHP_EOL;
exit;
}
// Reload the config
$this->runner->loadConfig();
gc_collect_cycles();
}
}
/**
* Fetch the child job by id.
*
* @param int $idNum The id of the job to find
* @return JobInterface
*/
public function job($idNum)
{
return $this->runner->getJobFromIdNum($idNum);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* A utility trait related to BatchDaemon functionality.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
trait BatchDaemonTrait
{
/**
* Returns whether or not the BatchDaemon is running.
*
* @return bool
*/
private function isDaemonRunning()
{
$isDaemonRunning = filter_var(
getenv('IS_BATCH_DAEMON_RUNNING'),
FILTER_VALIDATE_BOOLEAN
);
return $isDaemonRunning !== false;
}
}

View File

@@ -0,0 +1,207 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use Google\Cloud\Core\SysvTrait;
/**
* Represent batch jobs.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class BatchJob implements JobInterface
{
const DEFAULT_BATCH_SIZE = 100;
const DEFAULT_CALL_PERIOD = 2.0;
const DEFAULT_WORKERS = 1;
use JobTrait;
use SysvTrait;
use InterruptTrait;
use HandleFailureTrait;
/**
* @var callable The batch job handler. This callable accepts an array
* of items and should return a boolean.
*/
private $func;
/**
* @var int The size of the batch.
*/
private $batchSize;
/**
* @var float The period in seconds from the last execution to force
* executing the job.
*/
private $callPeriod;
/**
* @param string $identifier Unique identifier of the job.
* @param callable $func Any Callable except for Closure. The callable
* should accept an array of items as the first argument.
* @param int $idNum A numeric id for the job.
* @param array $options [optional] {
* Configuration options.
*
* @type int $batchSize The size of the batch. **Defaults to** `100`.
* @type float $callPeriod The period in seconds from the last execution
* to force executing the job. **Defaults to** `2.0`.
* @type int $numWorkers The number of child processes. It only takes
* effect with the {@see \Google\Cloud\Core\Batch\BatchDaemon}.
* **Defaults to** `1`.
* @type string $bootstrapFile A file to load before executing the
* job. It's needed for registering global functions.
* }
*/
public function __construct(
$identifier,
$func,
$idNum,
array $options = []
) {
$options += [
'batchSize' => self::DEFAULT_BATCH_SIZE,
'callPeriod' => self::DEFAULT_CALL_PERIOD,
'bootstrapFile' => null,
'numWorkers' => self::DEFAULT_WORKERS
];
$this->identifier = $identifier;
$this->func = $func;
$this->id = $idNum;
$this->batchSize = $options['batchSize'];
$this->callPeriod = $options['callPeriod'];
$this->bootstrapFile = $options['bootstrapFile'];
$this->numWorkers = $options['numWorkers'];
$this->initFailureFile();
}
/**
* Run the job.
*/
public function run()
{
$this->setupSignalHandlers();
$sysvKey = $this->getSysvKey($this->id);
$q = msg_get_queue($sysvKey);
$items = [];
$lastInvoked = microtime(true);
if (!is_null($this->bootstrapFile)) {
require_once($this->bootstrapFile);
}
while (true) {
// Fire SIGALRM after 1 second to unblock the blocking call.
pcntl_alarm(1);
if (msg_receive(
$q,
0,
$type,
8192,
$message,
true,
0, // blocking mode
$errorcode
)) {
if ($type === self::$typeDirect) {
$items[] = $message;
} elseif ($type === self::$typeFile) {
$items[] = unserialize(file_get_contents($message));
@unlink($message);
}
}
pcntl_signal_dispatch();
// It runs the job when
// 1. Number of items reaches the batchSize.
// 2-a. Count is >0 and the current time is larger than lastInvoked + period.
// 2-b. Count is >0 and the shutdown flag is true.
if ((count($items) >= $this->batchSize)
|| (count($items) > 0
&& (microtime(true) > $lastInvoked + $this->callPeriod
|| $this->shutdown))) {
printf(
'Running the job with %d items' . PHP_EOL,
count($items)
);
$this->flush($items);
$items = [];
$lastInvoked = microtime(true);
}
gc_collect_cycles();
if ($this->shutdown) {
return;
}
}
}
/**
* Finish any pending activity for this job.
*
* @param array $items
* @return bool
*/
public function flush(array $items = [])
{
if (! $this->callFunc($items)) {
$this->handleFailure($this->id, $items);
return false;
}
return true;
}
/**
* Finish any pending activity for this job.
*
* @access private
* @internal
*
* @param array $items
* @return bool
*/
public function callFunc(array $items = [])
{
return call_user_func_array($this->func, [$items]);
}
/**
* Returns the period in seconds from the last execution to force
* executing the job.
*
* @return float
*/
public function getCallPeriod()
{
return $this->callPeriod;
}
/**
* Returns the batch size.
*
* @return int
*/
public function getBatchSize()
{
return $this->batchSize;
}
}

View File

@@ -0,0 +1,212 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use Google\Cloud\Core\SysvTrait;
/**
* A class for executing jobs in batch.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class BatchRunner
{
use BatchDaemonTrait;
use SysvTrait;
/**
* @var JobConfig
*/
private $config;
/**
* @var ConfigStorageInterface
*/
private $configStorage;
/**
* @var ProcessItemInterface
*/
private $processor;
/**
* Determine internal implementation and loads the configuration.
*
* @param ConfigStorageInterface $configStorage [optional] The
* ConfigStorage object to use. **Defaults to** null. This is only
* for testing purpose.
* @param ProcessItemInterface $processor [optional] The processor object
* to use. **Defaults to** null. This is only for testing purpose.
*/
public function __construct(
?ConfigStorageInterface $configStorage = null,
?ProcessItemInterface $processor = null
) {
if ($configStorage === null || $processor === null) {
if ($this->isSysvIPCLoaded() && $this->isDaemonRunning()) {
$configStorage = new SysvConfigStorage();
$processor = new SysvProcessor();
} else {
$configStorage = InMemoryConfigStorage::getInstance();
$processor = $configStorage;
}
}
$this->configStorage = $configStorage;
$this->processor = $processor;
$this->loadConfig();
}
/**
* Register a job for batch execution.
*
* @param string $identifier Unique identifier of the job.
* @param callable $func Any Callable except for Closure. The callable
* should accept an array of items as the first argument.
* @param array $options [optional] {
* Configuration options.
*
* @type int $batchSize The size of the batch.
* @type float $callPeriod The period in seconds from the last execution
* to force executing the job.
* @type int $numWorkers The number of child processes. It only takes
* effect with the {@see \Google\Cloud\Core\Batch\BatchDaemon}.
* @type string $bootstrapFile A file to load before executing the
* job. It's needed for registering global functions.
* }
* @return bool true on success, false on failure
* @throws \InvalidArgumentException When receiving a Closure.
*/
public function registerJob($identifier, $func, array $options = [])
{
if ($func instanceof \Closure) {
throw new \InvalidArgumentException('Closure is not allowed');
}
// Always work on the latest data
$result = $this->configStorage->lock();
if ($result === false) {
return false;
}
$this->config = $this->configStorage->load();
$this->config->registerJob(
$identifier,
function ($id) use ($identifier, $func, $options) {
return new BatchJob($identifier, $func, $id, $options);
}
);
try {
$result = $this->configStorage->save($this->config);
} finally {
$this->configStorage->unlock();
}
return $result;
}
/**
* Submit an item.
*
* @param string $identifier Unique identifier of the job.
* @param mixed $item It needs to be serializable.
*
* @return void
* @throws \RuntimeException
*/
public function submitItem($identifier, $item)
{
$job = $this->getJobFromId($identifier);
if ($job === null) {
throw new \RuntimeException(
"The identifier does not exist: $identifier"
);
}
$idNum = $job->id();
$this->processor->submit($item, $idNum);
}
/**
* Get the job with the given identifier.
*
* @param string $identifier Unique identifier of the job.
*
* @return BatchJob|null
*/
public function getJobFromId($identifier)
{
return $this->config->getJobFromId($identifier);
}
/**
* Get the job with the given numeric id.
*
* @param int $idNum A numeric id of the job.
*
* @return BatchJob|null
*/
public function getJobFromIdNum($idNum)
{
return $this->config->getJobFromIdNum($idNum);
}
/**
* Get all the jobs.
*
* @return BatchJob[]
*/
public function getJobs()
{
return $this->config->getJobs();
}
/**
* Load the config from the storage.
*
* @return bool true on success
* @throws \RuntimeException when it fails to load the config.
*/
public function loadConfig()
{
$result = $this->configStorage->lock();
if ($result === false) {
throw new \RuntimeException('Failed to lock the configStorage');
}
try {
$result = $this->configStorage->load();
} catch (\RuntimeException $e) {
$this->configStorage->clear();
throw $e;
} finally {
$this->configStorage->unlock();
}
$this->config = $result;
return true;
}
/**
* Gets the item processor.
*
* @return ProcessItemInterface
*/
public function getProcessor()
{
return $this->processor;
}
}

View File

@@ -0,0 +1,199 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* A trait to assist in the registering and processing of batch jobs.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
trait BatchTrait
{
use SerializableClientTrait;
/**
* @var array
*/
private $batchOptions;
/**
* @var BatchRunner
*/
private $batchRunner;
/**
* @var string
*/
private $identifier;
/**
* @var string
*/
private $batchMethod;
/**
* @var bool
*/
private $debugOutput;
/**
* @var resource
*/
private $debugOutputResource;
/**
* Flushes items in the batch queue that have yet to be delivered. Please
* note this will have no effect when using the batch daemon.
*
* @return bool
*/
public function flush()
{
$id = $this->batchRunner
->getJobFromId($this->identifier)
->id();
return $this->batchRunner
->getProcessor()
->flush($id);
}
/**
* Deliver a list of items in a batch call.
*
* @param array $items
* @return bool
* @access private
*/
public function send(array $items)
{
$start = microtime(true);
try {
call_user_func_array($this->getCallback(), [$items]);
} catch (\Exception $e) {
if ($this->debugOutput) {
fwrite(
$this->debugOutputResource,
$e->getMessage() . PHP_EOL . PHP_EOL
. $e->getTraceAsString() . PHP_EOL
);
}
return false;
}
$end = microtime(true);
if ($this->debugOutput) {
fwrite(
$this->debugOutputResource,
sprintf(
'%f seconds for %s: %d items' . PHP_EOL,
$end - $start,
$this->batchMethod,
count($items)
)
);
fwrite(
$this->debugOutputResource,
sprintf(
'memory used: %d' . PHP_EOL,
memory_get_usage()
)
);
}
return true;
}
/**
* Returns an array representation of a callback which will be used to write
* batch items.
*
* @return array
*/
abstract protected function getCallback();
/**
* @param array $options [optional] {
* Configuration options.
*
* @type resource $debugOutputResource A resource to output debug output
* to. **Defaults to** `php://stderr`.
* @type bool $debugOutput Whether or not to output debug information.
* Please note that unless a debug output resource is configured
* this setting will only apply to CLI based applications.
* **Defaults to** `false`.
* @type array $batchOptions A set of options for a BatchJob.
* {@see \Google\Cloud\Core\Batch\BatchJob::__construct()} for
* more details.
* **Defaults to** ['batchSize' => 1000,
* 'callPeriod' => 2.0,
* 'numWorkers' => 2].
* @type array $clientConfig A config used to construct the client upon
* which requests will be made.
* @type BatchRunner $batchRunner A BatchRunner object. Mainly used for
* the tests to inject a mock. **Defaults to** a newly created
* BatchRunner.
* @type string $identifier An identifier for the batch job. This
* value must be unique across all job configs.
* @type string $batchMethod The name of the batch method used to
* deliver items.
* @type ClosureSerializerInterface $closureSerializer An implementation
* responsible for serializing closures used in the
* `$clientConfig`. This is especially important when using the
* batch daemon. **Defaults to**
* {@see \Google\Cloud\Core\Batch\OpisClosureSerializer} if the
* `opis/closure` library is installed.
* }
* @throws \InvalidArgumentException
*/
private function setCommonBatchProperties(array $options = [])
{
if (!isset($options['identifier'])) {
throw new \InvalidArgumentException(
'A valid identifier is required in order to register a job.'
);
}
if (!isset($options['batchMethod'])) {
throw new \InvalidArgumentException(
'A batchMethod is required.'
);
}
$this->setSerializableClientOptions($options);
$this->batchMethod = $options['batchMethod'];
$this->identifier = $options['identifier'];
$this->debugOutputResource = $options['debugOutputResource'] ?? fopen('php://stderr', 'w');
$this->debugOutput = $options['debugOutput'] ?? false;
$batchOptions = $options['batchOptions'] ?? [];
$this->batchOptions = $batchOptions + [
'batchSize' => 1000,
'callPeriod' => 2.0,
'numWorkers' => 2
];
$this->batchRunner = $options['batchRunner'] ?? new BatchRunner();
$this->batchRunner->registerJob(
$this->identifier,
[$this, 'send'],
$this->batchOptions
);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* An interface for serializing closures.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
interface ClosureSerializerInterface
{
/**
* Recursively serializes closures.
*
* @param mixed $data
*/
public function wrapClosures(&$data);
/**
* Recursively unserializes closures.
*
* @param mixed $data
*/
public function unwrapClosures(&$data);
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* An interface for storing the configuration.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
interface ConfigStorageInterface
{
/**
* locks the storage
*
* @return bool
*/
public function lock();
/**
* releases the lock
*
* @return bool
*/
public function unlock();
/**
* saves the JobConfig to the storage
* @param JobConfig $config A JobConfig to save.
* @return bool true on success, false on failure
*/
public function save(JobConfig $config);
/**
* loads the JobConfig from the storage
*
* @return JobConfig
* @throws \RuntimeException when failed to load the JobConfig.
*/
public function load();
/**
* Clear the JobConfig from storage.
*/
public function clear();
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* A utility trait for handling failed items.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
trait HandleFailureTrait
{
/**
* @var string A filename to save the failed items.
*/
private $failureFile;
/**
* @var string Base directory for the failure files.
*/
private $baseDir;
/**
* Determine the failureFile.
*/
private function initFailureFile()
{
$this->baseDir = getenv('GOOGLE_CLOUD_BATCH_DAEMON_FAILURE_DIR');
if ('false' === $this->baseDir) {
// setting the file to the string "false" will prevent logging of failed items
return;
}
if ($this->baseDir === false) {
$this->baseDir = sprintf(
'%s/batch-daemon-failure',
sys_get_temp_dir()
);
}
if (!is_dir($this->baseDir) && !@mkdir($this->baseDir, 0700, true) && !is_dir($this->baseDir)) {
throw new \RuntimeException(
sprintf(
'Could not create a directory: %s',
$this->baseDir
)
);
}
// Use getmypid for simplicity.
$this->failureFile = sprintf(
'%s/failed-items-%d',
$this->baseDir,
getmypid()
);
}
/**
* Save the items to the failureFile. We silently abandon the items upon
* failures in this method because there's nothing we can do.
*
* @param int $idNum A numeric id for the job.
* @param array $items Items to save.
*/
public function handleFailure($idNum, array $items)
{
if (!$this->failureFile) {
$this->initFailureFile();
}
if ($this->failureFile) {
$fp = @fopen($this->failureFile, 'a');
@fwrite($fp, json_encode(serialize([$idNum => $items])) . PHP_EOL);
@fclose($fp);
}
}
/**
* Get all the filenames for the failure files.
*
* @return array Filenames for all the failure files.
*/
private function getFailedFiles()
{
$pattern = sprintf('%s/failed-items-*', $this->baseDir);
return glob($pattern) ?: [];
}
}

View File

@@ -0,0 +1,221 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use BadMethodCallException;
/**
* In-memory ConfigStorageInterface implementation.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
final class InMemoryConfigStorage implements
ConfigStorageInterface,
ProcessItemInterface
{
use HandleFailureTrait;
/* @var JobConfig */
private $config;
/* @var array */
private $items = [];
/* @var array */
private $lastInvoked = [];
/* @var float */
private $created;
/* @var bool */
private $hasShutdownHookRegistered;
/**
* Singleton getInstance.
*
* @return InMemoryConfigStorage
*/
public static function getInstance()
{
static $instance = null;
if ($instance === null) {
$instance = new InMemoryConfigStorage();
}
return $instance;
}
/**
* To prevent serialize.
*/
public function __sleep()
{
throw new BadMethodCallException('Serialization not supported');
}
/**
* To prevent unserialize.
*/
public function __wakeup()
{
throw new BadMethodCallException('Serialization not supported');
}
/**
* To prevent cloning.
*/
private function __clone()
{
}
/**
* The constructor registers the shutdown function for running the job for
* remainder items when the script exits.
*/
private function __construct()
{
$this->config = new JobConfig();
$this->created = microtime(true);
$this->initFailureFile();
$this->hasShutdownHookRegistered = false;
}
/**
* Just return true
*
* @return bool
*/
public function lock()
{
return true;
}
/**
* Just return true
*
* @return bool
*/
public function unlock()
{
return true;
}
/**
* Save the given JobConfig.
*
* @param JobConfig $config A JobConfig to save.
* @return bool
*/
public function save(JobConfig $config)
{
$this->config = $config;
return true;
}
/**
* Load a JobConfig from the storage.
*
* @return JobConfig
* @throws \RuntimeException when failed to load the JobConfig.
*/
public function load()
{
return $this->config;
}
/**
* Clear the JobConfig from storage.
*/
public function clear()
{
$this->config = new JobConfig();
}
/**
* Hold the items in memory and run the job in the same process when it
* meets the condition.
*
* We want to delay registering the shutdown function. The error
* reporter also registers a shutdown function and the order matters.
*
* @see \Google\Cloud\ErrorReporting\Bootstrap::init()
* @see http://php.net/manual/en/function.register-shutdown-function.php
*
* @param mixed $item An item to submit.
* @param int $idNum A numeric id for the job.
* @return void
*/
public function submit($item, $idNum)
{
if (!$this->hasShutdownHookRegistered) {
register_shutdown_function([$this, 'shutdown']);
$this->hasShutdownHookRegistered = true;
}
if (!array_key_exists($idNum, $this->items)) {
$this->items[$idNum] = [];
$this->lastInvoked[$idNum] = $this->created;
}
$this->items[$idNum][] = $item;
$job = $this->config->getJobFromIdNum($idNum);
$batchSize = $job->getBatchSize();
$period = $job->getCallPeriod();
if ((count($this->items[$idNum]) >= $batchSize)
|| (count($this->items[$idNum]) !== 0
&& microtime(true) > $this->lastInvoked[$idNum] + $period)) {
$this->flush($idNum);
$this->items[$idNum] = [];
$this->lastInvoked[$idNum] = microtime(true);
}
}
/**
* Run the job with the given id.
*
* @param int $idNum A numeric id for the job.
* @return bool
*/
public function flush($idNum)
{
if (isset($this->items[$idNum])) {
$job = $this->config->getJobFromIdNum($idNum);
if (!$job->flush($this->items[$idNum])) {
$this->handleFailure($idNum, $this->items[$idNum]);
}
$this->items[$idNum] = [];
$this->lastInvoked[$idNum] = microtime(true);
}
return true;
}
/**
* Run the job for remainder items.
*/
public function shutdown()
{
foreach ($this->items as $idNum => $items) {
if (count($items) !== 0) {
$this->flush($idNum);
}
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* A trait to assist in handling interrupt signals and gracefully stopping work.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
trait InterruptTrait
{
private $shutdown = false;
private function setupSignalHandlers()
{
// setup signal handlers
pcntl_signal(SIGTERM, [$this, 'sigHandler']);
pcntl_signal(SIGINT, [$this, 'sigHandler']);
pcntl_signal(SIGHUP, [$this, 'sigHandler']);
pcntl_signal(SIGALRM, [$this, 'sigHandler']);
}
/**
* A signal handler for setting the terminate switch.
*
* @see http://php.net/manual/en/function.pcntl-signal.php
*
* @param int $signo The received signal.
* @param mixed $siginfo [optional] An array representing the signal
* information. **Defaults to** null.
*
* @return void
*/
public function sigHandler($signo, $siginfo = null)
{
switch ($signo) {
case SIGINT:
case SIGTERM:
$this->shutdown = true;
break;
}
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* Hold configurations for the {@see \Google\Cloud\Core\Batch\BatchRunner}.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class JobConfig
{
/**
* @var array Associative array of JobInterface instances keyed by
* identifier.
*/
private $jobs = [];
/**
* @var array[string]int Associative array of job identifier to job id.
*/
private $identifierToId = [];
/**
* @var array[int]string Associative array of job id to job identifier.
*/
private $idToIdentifier = [];
/**
* Get the job with the given identifier.
*
* @param string $identifier Unique identifier of the job.
*
* @return JobInterface|null
*/
public function getJobFromId($identifier)
{
return array_key_exists($identifier, $this->identifierToId)
? $this->jobs[$identifier]
: null;
}
/**
* Get the job with the given numeric id.
*
* @param int $idNum A numeric id of the job.
*
* @return JobInterface|null
*/
public function getJobFromIdNum($idNum)
{
return array_key_exists($idNum, $this->idToIdentifier)
? $this->jobs[$this->idToIdentifier[$idNum]]
: null;
}
/**
* Register a job for executing in batch.
*
* @param string $identifier Unique identifier of the job.
* @param callable $callback Callback that accepts the job $idNum
* and returns a JobInterface instance.
* @return void
*/
public function registerJob($identifier, $callback)
{
if (array_key_exists($identifier, $this->identifierToId)) {
$idNum = $this->identifierToId[$identifier];
} else {
$idNum = count($this->identifierToId) + 1;
$this->idToIdentifier[$idNum] = $identifier;
}
$this->jobs[$identifier] = call_user_func(
$callback,
$idNum
);
$this->identifierToId[$identifier] = $idNum;
}
/**
* Get all the jobs indexed by the job's identifier.
*
* @return array Associative array of JobInterface instances keyed by a
* string identifier.
*/
public function getJobs()
{
return $this->jobs;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* The JobInterface represents any job that can be serialized and run in a
* separate process via the Batch daemon.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
interface JobInterface
{
/**
* Runs the job loop. This is expected to be a blocking call.
*/
public function run();
/**
* Return the job identifier
*
* @return string
*/
public function identifier();
/**
* Returns the number of workers for this job.
*
* @return int
*/
public function numWorkers();
/**
* Finish any pending activity for this job.
*
* @param array $items
* @return bool
*/
public function flush(array $items = []);
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* A trait to assist in implementing the JobInterface
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
trait JobTrait
{
/**
* @var string The job identifier
*/
private $identifier;
/**
* @var int The job id
*/
private $id;
/**
* @var int The number of workers for this job.
*/
private $numWorkers;
/**
* @var string|null An optional file that is required to run this job.
*/
private $bootstrapFile;
/**
* Return the job identifier
*
* @return string
*/
public function identifier()
{
return $this->identifier;
}
/**
* Return the job id
*
* @return int
*/
public function id()
{
return $this->id;
}
/**
* Returns the number of workers for this job. **Defaults to* 1.
*
* @return int
*/
public function numWorkers()
{
return $this->numWorkers;
}
/**
* Returns the optional file required to run this job.
*
* @return string|null
*/
public function bootstrapFile()
{
return $this->bootstrapFile;
}
/**
* Runs the job loop. This is expected to be a blocking call.
*/
abstract public function run();
/**
* Finish any pending activity for this job.
*
* @param array $items
* @return bool
*/
public function flush(array $items = [])
{
return false;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use Opis\Closure\SerializableClosure;
/**
* A closure serializer utilizing
* [Opis Closure Library](https://github.com/opis/closure).
*
* @deprecated use OpisClosureSerializerV4
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class OpisClosureSerializer implements ClosureSerializerInterface
{
/**
* Recursively serializes closures.
*
* @param mixed $data
*/
public function wrapClosures(&$data)
{
SerializableClosure::enterContext();
SerializableClosure::wrapClosures($data);
SerializableClosure::exitContext();
}
/**
* Recursively unserializes closures.
*
* @param mixed $data
*/
public function unwrapClosures(&$data)
{
SerializableClosure::enterContext();
SerializableClosure::unwrapClosures($data);
SerializableClosure::exitContext();
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use Opis\Closure;
/**
* A closure serializer utilizing
* [Opis Closure Library](https://github.com/opis/closure).
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class OpisClosureSerializerV4 implements ClosureSerializerInterface
{
/**
* Recursively serializes closures.
*
* @param mixed $data
*/
public function wrapClosures(&$data)
{
$data = Closure\serialize($data);
}
/**
* Recursively unserializes closures.
*
* @param mixed $data
*/
public function unwrapClosures(&$data)
{
$data = Closure\unserialize($data);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* An interface for processing the items.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
interface ProcessItemInterface
{
/**
* Submit a job for async processing.
*
* @param mixed $item An item to submit.
* @param int $idNum A numeric id of the job.
* @return void
* @throws \RuntimeException when failed to store the item.
*/
public function submit($item, $idNum);
/**
* Run the job with the given id.
*
* @param int $idNum A numeric id of the job.
* @return bool
*/
public function flush($idNum);
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Copyright 2020 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* Exception thrown in {@see \Google\Cloud\Core\Batch\SysvProcessor::submit()}
* method when it cannot add an item to the message queue.
* Possible causes include:
*
* - batch daemon is not running
* - no job registered for this queue
* - items are submitted faster than a job can handle them
*/
class QueueOverflowException extends \RuntimeException
{
public function __construct()
{
parent::__construct('Item queue overflow. Check that the batch daemon is running.');
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* A class for retrying the failed items.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class Retry
{
use HandleFailureTrait;
/* @var BatchRunner */
private $runner;
/**
* Initialize a BatchRunner and $failureFile.
*
* @param BatchRunner $runner [optional] **Defaults to** a new BatchRunner.
*/
public function __construct(?BatchRunner $runner = null)
{
$this->runner = $runner ?: new BatchRunner();
$this->initFailureFile();
}
/**
* Retry all the failed items.
*/
public function retryAll()
{
foreach ($this->getFailedFiles() as $file) {
// Rename the file first
$tmpFile = dirname($file) . '/retrying-' . basename($file);
rename($file, $tmpFile);
$fp = @fopen($tmpFile, 'r');
if ($fp === false) {
fwrite(
STDERR,
sprintf('Could not open the file: %s' . PHP_EOL, $tmpFile)
);
continue;
}
while ($line = fgets($fp)) {
$jsonDecodedValue = json_decode($line);
// Check if data json_encoded after serialization
if ($jsonDecodedValue !== null || $jsonDecodedValue !== false) {
$line = $jsonDecodedValue;
}
$a = unserialize($line);
$idNum = key($a);
$job = $this->runner->getJobFromIdNum($idNum);
if (! $job->callFunc($a[$idNum])) {
$this->handleFailure($idNum, $a[$idNum]);
}
}
@fclose($fp);
@unlink($tmpFile);
}
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use Opis\Closure\SerializableClosure;
/**
* A trait to assist in serializing/deserializing client configuration that may
* contain closures.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
trait SerializableClientTrait
{
/**
* @var array
*/
private $clientConfig;
/**
* @var ClosureSerializerInterface|null
*/
private $closureSerializer;
/**
* @param array $options {
* Configuration options.
*
* @type ClosureSerializerInterface $closureSerializer An implementation
* responsible for serializing closures used in the
* `$clientConfig`. This is especially important when using the
* batch daemon. **Defaults to**
* {@see \Google\Cloud\Core\Batch\OpisClosureSerializer} if the
* `opis/closure` library is installed.
* @type array $clientConfig A config used to construct the client upon
* which requests will be made.
* }
*/
private function setSerializableClientOptions(array $options)
{
$options += [
'closureSerializer' => null,
'clientConfig' => []
];
$this->closureSerializer = $options['closureSerializer']
?? $this->getDefaultClosureSerializer();
$this->setWrappedClientConfig($options);
}
/**
* @param array $options
*/
private function setWrappedClientConfig(array $options)
{
$config = $options['clientConfig'] ?? [];
if ($config && $this->closureSerializer) {
$this->closureSerializer->wrapClosures($config);
}
$this->clientConfig = $config;
}
/**
* @return array
*/
private function getUnwrappedClientConfig()
{
if ($this->clientConfig && $this->closureSerializer) {
$this->closureSerializer->unwrapClosures($this->clientConfig);
}
return $this->clientConfig;
}
/**
* @return ClosureSerializerInterface|null
*/
private function getDefaultClosureSerializer()
{
if (function_exists('Opis\Closure\serialize')) {
return new OpisClosureSerializerV4();
}
if (class_exists(SerializableClosure::class)) {
return new OpisClosureSerializer();
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* Represents a simple job that runs a single method that loops forever.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class SimpleJob implements JobInterface
{
use JobTrait;
/**
* @var callable
*/
private $func;
/**
* Creates a new Simple Job.
*
* @param string $identifier Unique identifier of the job.
* @param callable $func Any Callable except for Closure. The callable
* should not accept arguments and should loop forever.
* @param int $id The job id.
* @param array $options [optional] {
* Configuration options.
*
* @type string $bootstrapFile A file to load before executing the job.
* It's needed for registering global functions.
* @type int $numWorkers The number of workers for this job.
* }
*/
public function __construct($identifier, $func, $id, array $options = [])
{
$this->identifier = $identifier;
$this->func = $func;
$this->id = $id;
$options += [
'bootstrapFile' => null,
'numWorkers' => 1
];
$this->numWorkers = $options['numWorkers'];
$this->bootstrapFile = $options['bootstrapFile'];
}
/**
* Runs the job loop. This is expected to be a blocking call.
*/
public function run()
{
if ($this->bootstrapFile) {
require_once $this->bootstrapFile;
}
call_user_func($this->func);
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use Google\Cloud\Core\SysvTrait;
/**
* A trait to assist in the registering and processing of simple jobs.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
trait SimpleJobTrait
{
use BatchDaemonTrait;
use SysvTrait;
use SerializableClientTrait;
/**
* The simple loop function. This method is expected to be a blocking call.
*/
abstract public function run();
/**
* Registers this object as a SimpleJob.
*
* @param array $options [optional] {
* Configuration options.
*
* @type string $identifier An identifier for the simple job. This
* value must be unique across all job configs.
* @type ConfigStorageInterface $configStorage The configuration storage
* used to save configuration.
* @type int $numWorkers The number of workers for this job.
* @type array $clientConfig A config used to construct the client upon
* which requests will be made.
* @type ClosureSerializerInterface $closureSerializer An implementation
* responsible for serializing closures used in the
* `$clientConfig`. This is especially important when using the
* batch daemon. **Defaults to**
* {@see \Google\Cloud\Core\Batch\OpisClosureSerializer} if the
* `opis/closure` library is installed.
* }
*/
private function setSimpleJobProperties(array $options = [])
{
if (!isset($options['identifier'])) {
throw new \InvalidArgumentException(
'A valid identifier is required in order to register a job.'
);
}
$options += [
'configStorage' => null,
];
$this->setSerializableClientOptions($options);
$identifier = $options['identifier'];
$configStorage = $options['configStorage'] ?: $this->defaultConfigStorage();
$result = $configStorage->lock();
if ($result === false) {
return false;
}
$config = $configStorage->load();
$config->registerJob(
$identifier,
function ($id) use ($identifier, $options) {
return new SimpleJob($identifier, [$this, 'run'], $id, $options);
}
);
try {
$result = $configStorage->save($config);
} finally {
$configStorage->unlock();
}
return $result;
}
private function defaultConfigStorage()
{
if ($this->isSysvIPCLoaded() && $this->isDaemonRunning()) {
return new SysvConfigStorage();
} else {
return InMemoryConfigStorage::getInstance();
}
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
/**
* ConfigStorageInterface implementation with SystemV IPC shared memory.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class SysvConfigStorage implements ConfigStorageInterface
{
const VAR_KEY = 1;
const DEFAULT_SHM_SIZE = 200000;
const DEFAULT_PERM = 0600;
const DEFAULT_PROJECT = 'A';
/* @var int */
private $sysvKey;
/* @var int */
private $semid;
/* @var int */
private $shmSize;
/* @var int */
private $perm;
/* @var string */
private $project;
/**
* Prepare the key for semaphore and shared memory.
*/
public function __construct()
{
$this->shmSize = intval(getenv('GOOGLE_CLOUD_BATCH_SHM_SIZE'));
if ($this->shmSize === 0) {
$this->shmSize = self::DEFAULT_SHM_SIZE;
}
$this->perm = octdec(getenv('GOOGLE_CLOUD_BATCH_PERM'));
if ($this->perm === 0) {
$this->perm = self::DEFAULT_PERM;
}
$this->project = getenv('GOOGLE_CLOUD_BATCH_PROJECT');
if ($this->project === false) {
$this->project = self::DEFAULT_PROJECT;
}
$this->sysvKey = ftok(__FILE__, $this->project);
$this->semid = sem_get($this->sysvKey, 1, $this->perm, 1);
}
/**
* Acquire a lock.
*
* @return bool
*/
public function lock()
{
return sem_acquire($this->semid);
}
/**
* Release a lock.
*
* @return bool
*/
public function unlock()
{
return sem_release($this->semid);
}
/**
* Save the given JobConfig.
*
* @param JobConfig $config A JobConfig to save.
* @return bool
* @throws \RuntimeException when failed to attach to the shared memory or serialization fails
*/
public function save(JobConfig $config)
{
$shmid = shm_attach($this->sysvKey, $this->shmSize, $this->perm);
if ($shmid === false) {
throw new \RuntimeException(
'Failed to attach to the shared memory'
);
}
// If the variable write fails, clear the memory and re-raise the exception
try {
$result = shm_put_var($shmid, self::VAR_KEY, $config);
} catch (\Exception $e) {
$this->clear();
throw new \RuntimeException($e->getMessage());
} finally {
shm_detach($shmid);
}
return $result;
}
/**
* Load a JobConfig from the storage.
*
* @return JobConfig
* @throws \RuntimeException when failed to attach to the shared memory or deserialization fails
*/
public function load()
{
$shmid = shm_attach($this->sysvKey, $this->shmSize, $this->perm);
if ($shmid === false) {
throw new \RuntimeException(
'Failed to attach to the shared memory'
);
}
if (! shm_has_var($shmid, self::VAR_KEY)) {
$result = new JobConfig();
} else {
$result = shm_get_var($shmid, self::VAR_KEY);
}
shm_detach($shmid);
if ($result === false) {
throw new \RuntimeException(
'Failed to deserialize data from shared memory'
);
}
return $result;
}
/**
* Clear the JobConfig from storage.
*/
public function clear()
{
$shmid = shm_attach($this->sysvKey, $this->shmSize, $this->perm);
shm_remove_var($shmid, self::VAR_KEY);
}
/**
* Serialize the object
*/
public function __serialize()
{
$vars = get_object_vars($this);
// As of PHP 8.0, "semid" is the unserializable object "SysvSemaphore"
// @see https://github.com/googleapis/google-cloud-php/issues/3749
unset($vars['semid']);
return $vars;
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Batch;
use Google\Cloud\Core\SysvTrait;
/**
* ProcessItemInterface implementation with SysV IPC message queue.
*
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class SysvProcessor implements ProcessItemInterface
{
use BatchDaemonTrait;
use SysvTrait;
/* @var array */
private $sysvQs = [];
/**
* Submit an item for async processing.
*
* @param mixed $item An item to submit.
* @param int $idNum A numeric id of the job.
* @return void
*
* @throws \RuntimeException when failed to store the item.
*/
public function submit($item, $idNum)
{
if (!array_key_exists($idNum, $this->sysvQs)) {
$this->sysvQs[$idNum] =
msg_get_queue($this->getSysvKey($idNum));
}
$result = @msg_send(
$this->sysvQs[$idNum],
self::$typeDirect,
$item,
true,
false
);
if ($result === false) {
// Try to put the content in a temp file and send the filename.
$tempFile = tempnam(sys_get_temp_dir(), 'Item');
$result = file_put_contents($tempFile, serialize($item));
if ($result === false) {
throw new \RuntimeException(
"Failed to write to $tempFile while submiting the item"
);
}
$result = @msg_send(
$this->sysvQs[$idNum],
self::$typeFile,
$tempFile,
true,
false
);
if ($result === false) {
@unlink($tempFile);
throw new QueueOverflowException();
}
}
}
/**
* Run the job with the given id. This has no effect and simply always
* returns false when using the batch daemon.
*
* @param int $idNum A numeric id of the job.
* @return bool
*/
public function flush($idNum)
{
return false;
}
}

95
vendor/google/cloud-core/src/Blob.php vendored Normal file
View File

@@ -0,0 +1,95 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\StreamInterface;
/**
* Represents a Blob value.
*
* Blobs can be used to store binary across data various Google Cloud services.
*
* Example:
* ```
* use Google\Cloud\Core\Blob;
*
* $blob = new Blob(file_get_contents(__DIR__ .'/family-photo.jpg'));
* ```
*
* ```
* // Get the value of a blob by casting to a string.
*
* echo (string) $blob;
* ```
*/
class Blob implements \JsonSerializable
{
/**
* @var mixed
*/
private $value;
/**
* Create a blob
*
* @param string|resource|StreamInterface $value The blob value
*/
public function __construct($value)
{
$this->value = Utils::streamFor($value);
}
/**
* Get the blob contents as a stream
*
* Example:
* ```
* $value = $blob->get();
* ```
*
* @return StreamInterface
*/
public function get()
{
return $this->value;
}
/**
* Cast the blob to a string
*
* @access private
* @return string
*/
public function __toString()
{
return (string) $this->value;
}
/**
* Implement JsonSerializable by returning a base64 encoded string of the blob
*
* @return string
* @access private
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return base64_encode((string) $this->value);
}
}

View File

@@ -0,0 +1,284 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\ProjectIdProviderInterface;
use Google\Cloud\Core\Compute\Metadata;
use Google\Cloud\Core\Exception\GoogleException;
/**
* Provides functionality common to each service client.
*/
trait ClientTrait
{
use JsonTrait;
/**
* @var string|null The project ID created in the Google Developers Console.
*/
private $projectId;
/**
* Get either a gRPC or REST connection based on the provided config
* and the system dependencies available.
*
* @param array $config
* @return string
* @throws GoogleException
*/
private function getConnectionType(array $config)
{
$isGrpcExtensionLoaded = $this->isGrpcLoaded();
$defaultTransport = $isGrpcExtensionLoaded ? 'grpc' : 'rest';
$transport = strtolower($config['transport'] ?? $defaultTransport);
if ($transport === 'grpc') {
if (!$isGrpcExtensionLoaded) {
throw new GoogleException(
'gRPC support has been requested but required dependencies ' .
'have not been found. ' . $this->getGrpcInstallationMessage()
);
}
}
return $transport;
}
/**
* Throw an exception if the gRPC extension is not loaded.
*
* @throws GoogleException
*/
private function requireGrpc()
{
if (!$this->isGrpcLoaded()) {
throw new GoogleException(
'The requested client requires the gRPC extension. ' .
$this->getGrpcInstallationMessage()
);
}
}
/**
* @return string
*/
private function getGrpcInstallationMessage()
{
return 'Please see https://cloud.google.com/php/grpc for installation ' .
'instructions.';
}
/**
* Fetch and validate the keyfile and set the project ID.
*
* @param array $config
* @return array
* @throws GoogleException
*/
private function configureAuthentication(array $config)
{
$credentialsFetcher = $config['credentialsFetcher'] ?? null;
if (!($credentialsFetcher instanceof FetchAuthTokenInterface)) {
$config['keyFile'] = $this->getKeyFile($config);
}
$this->projectId = $this->detectProjectId($config);
return $config;
}
/**
* Get a keyfile if it exists.
*
* Process:
* 1. If $config['keyFile'] is set, use that.
* 2. If $config['keyFilePath'] is set, load the file and use that.
* 3. If GOOGLE_APPLICATION_CREDENTIALS environment variable is set, load
* from that location and use that.
* 4. If OS-specific well-known-file is set, load from that location and use
* that.
*
* @param array $config
* @return array|null Key data
* @throws GoogleException
*/
private function getKeyFile(array $config = [])
{
$config += [
'keyFile' => null,
'keyFilePath' => null,
];
if ($config['keyFile']) {
return $config['keyFile'];
}
if ($config['keyFilePath']) {
if (!file_exists($config['keyFilePath'])) {
throw new GoogleException(sprintf(
'Given keyfile path %s does not exist',
$config['keyFilePath']
));
}
try {
$keyFileData = $this->jsonDecode(file_get_contents($config['keyFilePath']), true);
} catch (\InvalidArgumentException $ex) {
throw new GoogleException(sprintf(
'Given keyfile at path %s was invalid',
$config['keyFilePath']
));
}
return $keyFileData;
}
return CredentialsLoader::fromEnv()
?: CredentialsLoader::fromWellKnownFile();
}
/**
* Detect and return a project ID.
*
* Process:
* 1. If $config['projectId'] is set, use that.
* 2. If an emulator is enabled, return a dummy value.
* 3. If $config['keyFile'] is set, attempt to retrieve a project ID from
* that.
* 4. Check `GOOGLE_CLOUD_PROJECT` environment variable.
* 5. Check `GCLOUD_PROJECT` environment variable.
* 6. If code is running on compute engine, try to get the project ID from
* the metadata store.
* 7. Throw exception.
*
* @param array $config
* @return string
* @throws GoogleException
*/
private function detectProjectId(array $config)
{
$config += [
'credentialsFetcher' => null,
'httpHandler' => null,
'projectId' => null,
'projectIdRequired' => false,
'hasEmulator' => false,
'preferNumericProjectId' => false,
'suppressKeyFileNotice' => false
];
if ($config['projectId']) {
return $config['projectId'];
}
if ($config['hasEmulator']) {
return 'emulator-project';
}
if ($config['credentialsFetcher'] instanceof ProjectIdProviderInterface) {
return $config['credentialsFetcher']->getProjectId();
}
if (isset($config['keyFile'])) {
if (isset($config['keyFile']['project_id'])) {
return $config['keyFile']['project_id'];
}
if ($config['suppressKeyFileNotice'] !== true) {
$serviceAccountUri = 'https://cloud.google.com/iam/docs/' .
'creating-managing-service-account-keys#creating_service_account_keys';
trigger_error(
sprintf(
'A keyfile was given, but it does not contain a project ' .
'ID. This can indicate an old and obsolete keyfile, ' .
'in which case you should create a new one. To suppress ' .
'this message, set `suppressKeyFileNotice` to `true` in your client configuration. ' .
'To learn more about generating new keys, see this URL: %s',
$serviceAccountUri
),
E_USER_NOTICE
);
}
}
if (getenv('GOOGLE_CLOUD_PROJECT')) {
return getenv('GOOGLE_CLOUD_PROJECT');
}
if (getenv('GCLOUD_PROJECT')) {
return getenv('GCLOUD_PROJECT');
}
if ($this->onGce($config['httpHandler'])) {
$metadata = $this->getMetaData();
$projectId = $config['preferNumericProjectId']
? $metadata->getNumericProjectId()
: $metadata->getProjectId();
if ($projectId) {
return $projectId;
}
}
if ($config['projectIdRequired']) {
throw new GoogleException(
'No project ID was provided, ' .
'and we were unable to detect a default project ID.'
);
}
return '';
}
/**
* Abstract the GCECredentials call so we can mock it in the unit tests!
*
* @codeCoverageIgnore
* @return bool
*/
protected function onGce($httpHandler)
{
return GCECredentials::onGce($httpHandler);
}
/**
* Abstract the Metadata instantiation for unit testing
*
* @codeCoverageIgnore
* @return Metadata
*/
protected function getMetaData()
{
return new Metadata();
}
/**
* Abstract the checking of the grpc extension for unit testing.
*
* @codeCoverageIgnore
* @return bool
*/
protected function isGrpcLoaded()
{
return extension_loaded('grpc');
}
}

View File

@@ -0,0 +1,165 @@
<?php
/**
* Copyright 2015 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\Compute;
use Google\Cloud\Core\Compute\Metadata\Readers\HttpHandlerReader;
use Google\Cloud\Core\Compute\Metadata\Readers\ReaderInterface;
/**
* A library for accessing the Google Compute Engine (GCE) metadata.
*
* The metadata is available from Google Compute Engine instances and
* App Engine Managed VMs instances.
*
* Example:
* ```
* use Google\Cloud\Core\Compute\Metadata;
*
* $metadata = new Metadata();
* $projectId = $metadata->getProjectId();
* ```
*
* ```
* // It is easy to get any metadata from a project.
* $val = $metadata->getProjectMetadata($key);
* ```
*/
class Metadata
{
/**
* @var ReaderInterface The metadata reader.
*/
private $reader;
/**
* @var string The project id.
*/
private $projectId;
/**
* @var int The numeric project id.
*/
private $numericProjectId;
/**
* @param ReaderInterface $reader [optional] A metadata reader implementation.
*/
public function __construct(?ReaderInterface $reader = null)
{
$this->reader = $reader ?: new HttpHandlerReader();
}
/**
* Replace the default reader implementation
*
* @deprecated If a custom reader implementation is desired, provide it at
* construction.
* @param ReaderInterface $reader The reader implementation
*/
public function setReader(ReaderInterface $reader)
{
$this->reader = $reader;
}
/**
* Fetch a metadata item by its path
*
* Example:
* ```
* $projectId = $metadata->get('project/project-id');
* ```
*
* @param string $path The path of the item to retrieve.
*/
public function get($path)
{
return $this->reader->read($path);
}
/**
* Detect and return the project ID
*
* Example:
* ```
* $projectId = $metadata->getProjectId();
* ```
*
* @return string
*/
public function getProjectId()
{
if (!isset($this->projectId)) {
$this->projectId = $this->get('project/project-id');
}
return $this->projectId;
}
/**
* Detect and return the numeric project ID
*
* Example:
* ```
* $projectId = $metadata->getNumericProjectId();
* ```
*
* @return string
*/
public function getNumericProjectId()
{
if (!isset($this->numericProjectId)) {
$this->numericProjectId = $this->get('project/numeric-project-id');
}
return $this->numericProjectId;
}
/**
* Fetch an item from the project metadata
*
* Example:
* ```
* $foo = $metadata->getProjectMetadata('foo');
* ```
*
* @param string $key The metadata key
* @return string
*/
public function getProjectMetadata($key)
{
$path = 'project/attributes/' . $key;
return $this->get($path);
}
/**
* Fetch an item from the instance metadata
*
* Example:
* ```
* $foo = $metadata->getInstanceMetadata('foo');
* ```
*
* @param string $key The instance metadata key
* @return string
*/
public function getInstanceMetadata($key)
{
$path = 'instance/attributes/' . $key;
return $this->get($path);
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* Copyright 2019 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Compute\Metadata\Readers;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7\Request;
/**
* Read Compute Metadata using the HTTP Handler utility.
*/
class HttpHandlerReader implements ReaderInterface
{
/**
* @var callable
*/
private $httpHandler;
/**
* @param callable $httpHandler [optional] An HTTP Handler capable of
* accepting PSR7 requests and returning PSR7 responses.
*/
public function __construct(?callable $httpHandler = null)
{
$this->httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}
/**
* Read the metadata for a given path.
*
* @param string $path The metadata path, relative to `/computeMetadata/v1/`.
* @return string
*/
public function read($path)
{
$url = sprintf(
'http://%s/computeMetadata/v1/%s',
GCECredentials::METADATA_IP,
$path
);
$request = new Request('GET', $url, [
GCECredentials::FLAVOR_HEADER => 'Google'
]);
$handler = $this->httpHandler;
$res = $handler($request);
return (string) $res->getBody();
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright 2016 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\Compute\Metadata\Readers;
/**
* Defines a metadata reader implementation.
*/
interface ReaderInterface
{
/**
* Read metadata from a given path.
*/
public function read($path);
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* Copyright 2015 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\Compute\Metadata\Readers;
use Google\Auth\Credentials\GCECredentials;
/**
* A class only reading the metadata URL with an appropriate header.
*
* This class makes it easy to test the MetadataStream class.
*/
class StreamReader implements ReaderInterface
{
/**
* The base PATH for the metadata.
*
* @deprecated
*/
const BASE_URL = 'http://169.254.169.254/computeMetadata/v1/';
/**
* The header whose presence indicates GCE presence.
*
* @deprecated
*/
const FLAVOR_HEADER = 'Metadata-Flavor: Google';
/**
* A common context for this reader.
*/
private $context;
/**
* We create the common context in the constructor.
*/
public function __construct()
{
$options = [
'http' => [
'method' => 'GET',
'header' => GCECredentials::FLAVOR_HEADER . ': Google'
],
];
$this->context = $this->createStreamContext($options);
}
/**
* Read the metadata for a given path.
*
* @param string $path The metadata path, relative to `/computeMetadata/v1/`.
* @return string
*/
public function read($path)
{
$url = sprintf(
'http://%s/computeMetadata/v1/%s',
GCECredentials::METADATA_IP,
$path
);
return $this->getMetadata($url);
}
/**
* Abstracted for testing.
*
* @param array $options
* @return resource
* @codeCoverageIgnore
*/
protected function createStreamContext(array $options)
{
return stream_context_create($options);
}
/**
* Abstracted for testing.
*
* @param string $url
* @return string
* @codeCoverageIgnore
*/
protected function getMetadata($url)
{
return file_get_contents($url, false, $this->context);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Methods to control concurrent updates.
*/
trait ConcurrencyControlTrait
{
/**
* Apply the If-Match header to requests requiring concurrency control.
*
* @param array $options
* @param string $argName
* @return array
*/
private function applyEtagHeader(array $options, $argName = 'etag')
{
if (isset($options[$argName])) {
if (!isset($options['restOptions'])) {
$options['restOptions'] = [];
}
if (!isset($options['restOptions']['headers'])) {
$options['restOptions']['headers'] = [];
}
$options['restOptions']['headers']['If-Match'] = $options[$argName];
}
return $options;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Provides easier to read debug information when dumping a class to stdout.
*
* @codeCoverageIgnore
*/
trait DebugInfoTrait
{
/**
* @access private
* @return array
*/
public function __debugInfo()
{
$props = get_object_vars($this);
if (isset($this->connection)) {
$props['connection'] = get_class($this->connection);
}
if (isset($props['__excludeFromDebug'])) {
$exclude = $props['__excludeFromDebug'];
unset($props['__excludeFromDebug']);
foreach ($exclude as $e) {
unset($props[$e]);
}
}
return $props;
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\Auth\ProjectIdProviderInterface;
use Google\Cloud\Core\Exception\GoogleException;
/**
* @internal
* Provides functionality common to handwritten clients.
*/
trait DetectProjectIdTrait
{
/**
* @var string|null The project ID created in the Google Developers Console.
*/
private $projectId;
/**
* Detect and return a project ID.
* This is different from detectProjectId as this method is supposed to be used by
* handwritten clients delegating their auth process to GAX.
*
* Process:
* 1. If $config['projectId'] is set, use that.
* 2. If an emulator is enabled, return a dummy value.
* 3. If $config['credentials'] is set, attempt to retrieve a project ID from
* that.
* 4. If code is running on compute engine, try to get the project ID from
* the metadata store.
* 5. Throw exception.
*
* @param array $config
* @return string|int|null
* @throws GoogleException
*/
private function detectProjectId(array $config)
{
$config += [
'httpHandler' => null,
'projectId' => null,
'projectIdRequired' => false,
'hasEmulator' => false,
'credentials' => null,
];
if ($config['projectId']) {
return $config['projectId'];
}
if ($config['hasEmulator']) {
return 'emulator-project';
}
if ($config['credentials']
&& $config['credentials'] instanceof ProjectIdProviderInterface
&& $projectId = $config['credentials']->getProjectId()) {
return $projectId;
}
if (getenv('GOOGLE_CLOUD_PROJECT')) {
return getenv('GOOGLE_CLOUD_PROJECT');
}
if (getenv('GCLOUD_PROJECT')) {
return getenv('GCLOUD_PROJECT');
}
$this->throwExceptionIfProjectIdRequired($config);
return '';
}
/**
* Throws an exception if project id is required.
* @param array $config
* @throws GoogleException
*/
private function throwExceptionIfProjectIdRequired(array $config)
{
if ($config['projectIdRequired']) {
throw new GoogleException(
'No project ID was provided, ' .
'and we were unable to detect a default project ID.'
);
}
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Represents a Duration type.
*
* Example:
* ```
* use Google\Cloud\Core\Duration;
*
* $seconds = 100;
* $nanoSeconds = 1000;
* $duration = new Duration($seconds, $nanoSeconds);
* ```
*
* ```
* // Duration objects can be cast to json-encoded strings.
* echo (string) $duration;
* ```
*/
class Duration
{
const TYPE = 'DURATION';
/**
* @var int
*/
private $seconds;
/**
* @var int
*/
private $nanos;
/**
* @param int $seconds The number of seconds in the duration.
* @param int $nanos [optional] 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
];
}
/**
* Format the value as a string.
*
* Example:
* ```
* echo $duration->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();
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Provides common logic for configuring the usage of an emulator.
*/
trait EmulatorTrait
{
/**
* Configure the gapic configuration to use a service emulator.
*
* @param string $emulatorHost
* @return array
*/
private function emulatorGapicConfig($emulatorHost)
{
// Strip the URL scheme from the input, if it was provided.
if ($scheme = parse_url($emulatorHost, PHP_URL_SCHEME)) {
$search = $scheme . '://';
$emulatorHost = str_replace($search, '', $emulatorHost);
}
$config = [
'apiEndpoint' => $emulatorHost,
'credentials' => new InsecureCredentialsWrapper(),
];
if (class_exists('Grpc\ChannelCredentials')) {
$config['transportConfig'] = [
'grpc' => [
'stubOpts' => [
'credentials' => \Grpc\ChannelCredentials::createInsecure()
]
]
];
}
return $config;
}
/**
* Retrieve a valid base uri for a service emulator.
*
* @param string $emulatorHost
* @return string
*/
private function emulatorBaseUri($emulatorHost)
{
$emulatorUriComponents = parse_url($emulatorHost);
$emulatorUriComponents = array_merge(['scheme' => 'http', 'port' => ''], $emulatorUriComponents);
$baseUri = "{$emulatorUriComponents['scheme']}://{$emulatorUriComponents['host']}";
$baseUri .= $emulatorUriComponents['port'] ? ":{$emulatorUriComponents['port']}/" : '/';
return $baseUri;
}
/**
* When emulators are enabled, use them as the service host.
*
* This method is deprecated and will be removed in a future major release.
*
* @param string $baseUri
* @param string $emulatorHost [optional]
* @return string
*
* @deprecated
* @access private
*/
public function getEmulatorBaseUri($baseUri, $emulatorHost = null)
{
if ($emulatorHost) {
$baseUri = $this->emulatorBaseUri($emulatorHost);
}
return $baseUri;
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a transaction is aborted.
*/
class AbortedException extends ServiceException
{
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a request fails due to an error in the request.
* In REST context, this exception indicates a status code 400.
*/
class BadRequestException extends ServiceException
{
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a request fails due to a conflict.
* In REST context, this exception indicates a status code 409.
*/
class ConflictException extends ServiceException
{
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a request takes too long to complete.
* In REST context, this exception indicates a status code 504.
*/
class DeadlineExceededException extends ServiceException
{
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a request fails due to a failed precondition.
* In REST context, this exception indicates a status code 412.
*/
class FailedPreconditionException extends ServiceException
{
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
use Exception;
/**
* Exception thrown when a request fails.
*/
class GoogleException extends Exception
{
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a resource is not found.
*/
class NotFoundException extends ServiceException
{
/**
* Allows overriding message for injection of Whitelist Notice.
*
* @param string $message the new message
* @return void
* @access private
*/
public function setMessage($message)
{
$this->message = $message;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
/**
* Exception thrown when a request fails due to an error on the server.
* In REST context, this exception indicates a status code 500.
*/
class ServerException extends ServiceException
{
}

View File

@@ -0,0 +1,205 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Exception;
use Exception;
use Google\ApiCore\ApiException;
/**
* Exception thrown when a request fails.
*/
class ServiceException extends GoogleException
{
// This is the value of the '@type' key for an ErrorInfo object returned
// in the exception response
const ERRORINFO_TYPE_REST = 'type.googleapis.com/google.rpc.ErrorInfo';
/**
* @var Exception|null
*/
private $serviceException;
/**
* @var array
*/
protected $metadata;
/**
* @var array|null
*/
private $errorInfo;
/**
* @var array|null
*/
private $errorInfoMetadata;
/**
* @var string|null
*/
private $errorReason;
/**
* Handle previous exceptions differently here.
*
* @param string|null $message
* @param int $code
* @param Exception|null $serviceException
* @param array $metadata [optional] Exception metadata.
*/
public function __construct(
$message = null,
$code = 0,
?Exception $serviceException = null,
array $metadata = []
) {
$this->serviceException = $serviceException;
$this->metadata = $metadata;
$this->errorInfo = null;
$this->errorInfoMetadata = null;
$this->errorReason = null;
parent::__construct($message ?: '', $code);
}
/**
* If $serviceException is set, return true.
*
* @return bool
*/
public function hasServiceException()
{
return (bool) $this->serviceException;
}
/**
* Return the service exception object.
*
* @return Exception|null
*/
public function getServiceException()
{
return $this->serviceException;
}
/**
* Get exception metadata.
*/
public function getMetadata()
{
return $this->metadata;
}
/**
* Returns the metadata from the ErrorInfo part of the exception
*
* @return array
*/
public function getErrorInfoMetadata()
{
// Only calc the metadata if we haven't cached it
if (is_null($this->errorInfoMetadata)) {
// For response originated from the GAPIC layer, the current exception would have
// an ApiException within itself
if ($this->getServiceException() instanceof ApiException) {
return $this->getServiceException()->getErrorInfoMetadata();
}
$errorInfo = $this->getErrorInfoFromRestException();
// Cache the result to be reused if needed
$this->errorInfoMetadata = $errorInfo['metadata'] ?? [];
}
return $this->errorInfoMetadata;
}
/**
* Returns the reason from the ErrorInfo part of the exception
*
* @return string
*/
public function getReason()
{
// Only calc the errorReason if we haven't cached it
if (is_null($this->errorReason)) {
// For a response originated from the GAPIC layer, the current exception would have
// an ApiException within itself
if ($this->getServiceException() instanceof ApiException) {
return $this->getServiceException()->getReason();
}
$errorInfo = $this->getErrorInfoFromRestException();
// Cache the result to be reused if needed
$this->errorReason = $errorInfo['reason'] ?? '';
}
return $this->errorReason;
}
/**
* Return the delay in seconds and nanos before retrying the failed request.
*
* @return array
*/
public function getRetryDelay()
{
$metadata = array_filter($this->metadata, function ($metadataItem) {
return array_key_exists('retryDelay', $metadataItem);
});
if (count($metadata) === 0) {
return ['seconds' => 0, 'nanos' => 0];
}
return $metadata[0]['retryDelay'] + [
'seconds' => 0,
'nanos' => 0
];
}
/**
* Helper to return the error info from an exception
* which is not raised from the GAPIC layer
*
* @return array
*/
private function getErrorInfoFromRestException()
{
// Only calc errorInfo if it isn't cached
if (is_null($this->errorInfo)) {
$this->errorInfo = [];
$arr = json_decode($this->getMessage(), true);
if (!isset($arr['error']['details'])) {
return $this->errorInfo;
}
foreach ($arr['error']['details'] as $row) {
if (isset($row['@type']) && $row['@type'] === self::ERRORINFO_TYPE_REST) {
// save this in cache
$this->errorInfo = $row;
break;
}
}
}
return $this->errorInfo;
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Exponential backoff implementation.
*/
class ExponentialBackoff
{
const MAX_DELAY_MICROSECONDS = 60000000;
/**
* @var int
*/
private $retries;
/**
* @var callable|null
*/
private $retryFunction;
/**
* @var callable
*/
private $delayFunction;
/**
* @var callable|null
*/
private $calcDelayFunction;
/**
* @var callable|null
*/
private $retryListener;
/**
* @param int $retries [optional] Number of retries for a failed request.
* @param callable $retryFunction [optional] returns bool for whether or not
* to retry
* @param callable $retryListener [optional] Runs after the
* $retryFunction. Unlike the $retryFunction,this function isn't
* responsible to decide if a retry should happen or not, but it gives the
* users flexibility to consume exception messages and add custom logic.
* Function definition should match:
* function (\Exception $e, int $attempt, array $arguments): array
* Ex: One might want to change headers on every retry, this function can
* be used to achieve such a functionality.
*/
public function __construct(
$retries = null,
?callable $retryFunction = null,
?callable $retryListener = null
) {
$this->retries = $retries !== null ? (int) $retries : 3;
$this->retryFunction = $retryFunction;
$this->retryListener = $retryListener;
// @todo revisit this approach
// @codeCoverageIgnoreStart
$this->delayFunction = static function ($delay) {
usleep($delay);
};
// @codeCoverageIgnoreEnd
}
/**
* 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;
$calcDelayFunction = $this->calcDelayFunction ?: [$this, 'calculateDelay'];
$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, $retryAttempt)) {
throw $exception;
}
}
if ($retryAttempt >= $this->retries) {
break;
}
$delayFunction($calcDelayFunction($retryAttempt));
$retryAttempt++;
if ($this->retryListener) {
// Developer can modify the $arguments using the retryListener
// callback.
call_user_func_array(
$this->retryListener,
[$exception, $retryAttempt, &$arguments]
);
}
}
}
throw $exception;
}
/**
* If not set, defaults to using `usleep`.
*
* @param callable $delayFunction
* @return void
*/
public function setDelayFunction(callable $delayFunction)
{
$this->delayFunction = $delayFunction;
}
/**
* If not set, defaults to using
* {@see \Google\Cloud\Core\ExponentialBackoff::calculateDelay()}.
*
* @param callable $calcDelayFunction
* @return void
*/
public function setCalcDelayFunction(callable $calcDelayFunction)
{
$this->calcDelayFunction = $calcDelayFunction;
}
/**
* Calculates exponential delay.
*
* @param int $attempt The attempt number used to calculate the delay.
* @return int
*/
public static function calculateDelay($attempt)
{
return min(
mt_rand(0, 1000000) + (pow(2, $attempt) * 1000000),
self::MAX_DELAY_MICROSECONDS
);
}
}

View File

@@ -0,0 +1,226 @@
<?php
/**
* Copyright 2016 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core;
use InvalidArgumentException;
/**
* Represents a geographical point.
*
* Unless specified otherwise, this must conform to the
* [WGS84](http://www.unoosa.org/pdf/icg/2012/template/WGS_84.pdf) standard.
* Values must be within normalized ranges.
*
* Example:
* ```
* use Google\Cloud\Core\GeoPoint;
*
* $point = new GeoPoint(37.423147, -122.085015);
* ```
*/
class GeoPoint implements \JsonSerializable
{
/**
* @var float
*/
private $latitude;
/**
* @var float
*/
private $longitude;
/**
* Create a GeoPoint.
*
* Ints will be converted to floats. Values not passing the `is_numeric()`
* check will result in an exception.
*
* @param float|int|null $latitude The GeoPoint Latitude. **Note** that
* `null` is not a generally valid value, and will throw an
* `InvalidArgumentException` unless `$allowNull` is set to `true`.
* @param float|int|null $longitude The GeoPoint Longitude. **Note** that
* `null` is not a generally valid value, and will throw an
* `InvalidArgumentException` unless `$allowNull` is set to `true`.
* @param bool $allowNull [optional] If true, null values will be allowed
* in the constructor only. This switch exists to handle a rare case
* wherein a geopoint may be empty and is not intended for use from
* outside the client. **Defaults to** `false`.
* @throws \InvalidArgumentException
*/
public function __construct($latitude, $longitude, $allowNull = false)
{
$this->latitude = $this->validateValue($latitude, 'latitude', $allowNull);
$this->longitude = $this->validateValue($longitude, 'longitude', $allowNull);
}
/**
* Get the latitude
*
* Example:
* ```
* $latitude = $point->latitude();
* ```
*
* @return float|null
*/
public function latitude()
{
$this->checkContext('latitude', func_get_args());
return $this->latitude;
}
/**
* Set the latitude
*
* Non-numeric values will result in an exception
*
* Example:
* ```
* $point->setLatitude(42.279594);
* ```
*
* @param int|float $latitude The new latitude
* @return GeoPoint
* @throws \InvalidArgumentException
*/
public function setLatitude($latitude)
{
$this->latitude = $this->validateValue($latitude, 'latitude');
return $this;
}
/**
* Get the longitude
*
* Example:
* ```
* $longitude = $point->longitude();
* ```
*
* @return float|null
*/
public function longitude()
{
$this->checkContext('longitude', func_get_args());
return $this->longitude;
}
/**
* Set the longitude
*
* Non-numeric values will result in an exception.
*
* Example:
* ```
* $point->setLongitude(-83.732124);
* ```
*
* @param float|int $longitude The new longitude value
* @return GeoPoint
* @throws \InvalidArgumentException
*/
public function setLongitude($longitude)
{
$this->longitude = $this->validateValue($longitude, 'longitude');
return $this;
}
/**
* Return a GeoPoint
*
* Example:
* ```
* $point = $point->point();
* ```
*
* @return array [LatLng](https://cloud.google.com/datastore/reference/rest/Shared.Types/LatLng)
*/
public function point()
{
return [
'latitude' => $this->latitude,
'longitude' => $this->longitude
];
}
/**
* Let people know if they accidentally use the getter in setter context.
*
* @param string $method the method name
* @param array $args The method arguments
* @throws \InvalidArgumentException
* @return void
*/
private function checkContext($method, array $args)
{
if (count($args) > 0) {
throw new InvalidArgumentException(sprintf(
'Calling method %s with arguments is unsupported.',
$method
));
}
}
/**
* Check a given value's validity as a coordinate.
*
* Numeric values will be cast to type `float`. All other values will raise
* an exception with the exception of `null`, if `$allowNull` is set to true.
*
* @param mixed $value The coordinate value.
* @param string $type The coordinate type for error reporting.
* @param bool $allowNull [optional] Whether null values should be allowed.
* **Defaults to** `false`.
* @return float|null
*/
private function validateValue($value, $type, $allowNull = false)
{
if (!is_numeric($value) && (!$allowNull || ($allowNull && $value !== null))) {
throw new InvalidArgumentException(sprintf(
'Given %s must be a numeric value.',
$type
));
}
return $allowNull && $value === null
? $value
: (float) $value;
}
/**
* Implement JsonSerializable by representing GeoPoint as a JSON-object:
*
* ```
* {
* latitude: 31.778333
* longitude: 35.229722
* }
* ```
*
* @return object
* @access private
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return (object) $this->point();
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\ApiCore\ApiException;
use Google\ApiCore\Serializer;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Rpc\Code;
/**
* The GrpcRequestWrapper is responsible for delivering gRPC requests.
*/
class GrpcRequestWrapper
{
use RequestWrapperTrait;
use RequestProcessorTrait;
/**
* @var callable A handler used to deliver Psr7 requests specifically for
* authentication.
*/
private $authHttpHandler;
/**
* @var Serializer A serializer used to encode responses.
*/
private $serializer;
/**
* @var array gRPC specific configuration options passed off to the ApiCore
* library.
*/
private $grpcOptions;
/**
* @var array gRPC retry codes.
*/
private $grpcRetryCodes = [
Code::UNKNOWN,
Code::INTERNAL,
Code::UNAVAILABLE,
Code::DATA_LOSS
];
/**
* @param array $config [optional] {
* Configuration options. Please see
* {@see \Google\Cloud\Core\RequestWrapperTrait::setCommonDefaults()} for
* the other available options.
*
* @type callable $authHttpHandler A handler used to deliver Psr7
* requests specifically for authentication.
* @type Serializer $serializer A serializer used to encode responses.
* @type array $grpcOptions gRPC specific configuration options passed
* off to the ApiCore library.
* }
*/
public function __construct(array $config = [])
{
$this->setCommonDefaults($config);
$config += [
'authHttpHandler' => null,
'serializer' => new Serializer(),
'grpcOptions' => []
];
$this->authHttpHandler = $config['authHttpHandler'] ?: HttpHandlerFactory::build();
$this->serializer = $config['serializer'];
$this->grpcOptions = $config['grpcOptions'];
}
/**
* Deliver the request.
*
* @param callable $request The request to execute.
* @param array $args The arguments for the request.
* @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 callable $grpcRetryFunction Sets the conditions for whether or
* not a request should attempt to retry. Function signature should
* match: `function (\Exception $ex) : bool`.
* @type array $grpcOptions gRPC specific configuration options.
* }
* @return array
* @throws Exception\ServiceException
*/
public function send(callable $request, array $args, array $options = [])
{
$retries = $options['retries'] ?? $this->retries;
$retryFunction = $options['grpcRetryFunction']
?? function (\Exception $ex) {
$statusCode = $ex->getCode();
return in_array($statusCode, $this->grpcRetryCodes);
};
$grpcOptions = $options['grpcOptions'] ?? $this->grpcOptions;
$timeout = $options['requestTimeout'] ?? $this->requestTimeout;
$backoff = new ExponentialBackoff($retries, $retryFunction);
if (!isset($grpcOptions['retrySettings'])) {
$retrySettings = [
'retriesEnabled' => false
];
if ($timeout) {
$retrySettings['noRetriesRpcTimeoutMillis'] = $timeout * 1000;
}
$grpcOptions['retrySettings'] = $retrySettings;
}
$optionalArgs = &$args[count($args) - 1];
$optionalArgs += $grpcOptions;
try {
return $this->handleResponse($backoff->execute($request, $args));
} catch (\Exception $ex) {
if ($ex instanceof ApiException) {
throw $this->convertToGoogleException($ex);
}
throw $ex;
}
}
}

View File

@@ -0,0 +1,341 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\ApiCore\CredentialsWrapper;
use Google\Auth\GetUniverseDomainInterface;
use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Core\Exception\ServiceException;
use Google\Protobuf\NullValue;
/**
* Provides shared functionality for gRPC service implementations.
*/
trait GrpcTrait
{
use WhitelistTrait;
use ArrayTrait;
/**
* @var GrpcRequestWrapper Wrapper used to handle sending requests to the
* gRPC API.
*/
private $requestWrapper;
/**
* Sets the request wrapper.
*
* @param GrpcRequestWrapper $requestWrapper
*/
public function setRequestWrapper(GrpcRequestWrapper $requestWrapper)
{
$this->requestWrapper = $requestWrapper;
}
/**
* Get the GrpcRequestWrapper.
*
* @return GrpcRequestWrapper|null
*/
public function requestWrapper()
{
return $this->requestWrapper;
}
/**
* Delivers a request.
*
* @param callable $request
* @param array $args
* @param bool $whitelisted
* @return \Generator|array
* @throws ServiceException
*/
public function send(callable $request, array $args, $whitelisted = false)
{
$requestOptions = $this->pluckArray([
'grpcOptions',
'retries',
'requestTimeout',
'grpcRetryFunction'
], $args[count($args) - 1]);
try {
return $this->requestWrapper->send($request, $args, $requestOptions);
} catch (NotFoundException $e) {
if ($whitelisted) {
throw $this->modifyWhitelistedError($e);
}
throw $e;
}
}
/**
* Gets the default configuration for generated clients.
*
* @param string $version
* @param callable|null $authHttpHandler
* @param string|null $universeDomain
* @return array
*/
private function getGaxConfig(
$version,
?callable $authHttpHandler = null,
?string $universeDomain = null
) {
$config = [
'libName' => 'gccl',
'libVersion' => $version,
'transport' => 'grpc'
];
// GAX v0.32.0 introduced the CredentialsWrapper class and a different
// way to configure credentials. If the class exists, use this new method
// otherwise default to legacy usage.
if (class_exists(CredentialsWrapper::class)) {
$config['credentials'] = new CredentialsWrapper(
$this->requestWrapper->getCredentialsFetcher(),
$authHttpHandler,
// If the universe domain hasn't been explicitly set, check the the environment variable,
// otherwise assume GDU ("googleapis.com").
$universeDomain
?: getenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN')
?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN
);
} else {
$config += [
'credentialsLoader' => $this->requestWrapper->getCredentialsFetcher(),
'authHttpHandler' => $authHttpHandler,
'enableCaching' => false
];
}
return $config;
}
use TimeTrait;
/**
* Format a struct for the API.
*
* @param array $fields
* @return array
*/
private function formatStructForApi(array $fields)
{
$fFields = [];
foreach ($fields as $key => $value) {
$fFields[$key] = $this->formatValueForApi($value);
}
return ['fields' => $fFields];
}
private function unpackStructFromApi(array $struct)
{
$vals = [];
foreach ($struct['fields'] as $key => $val) {
$vals[$key] = $this->unpackValue($val);
}
return $vals;
}
private function unpackValue($value)
{
if (count($value) > 1) {
throw new \RuntimeException("Unexpected fields in struct: $value");
}
foreach ($value as $setField => $setValue) {
switch ($setField) {
case 'listValue':
$valueList = [];
foreach ($setValue['values'] as $innerValue) {
$valueList[] = $this->unpackValue($innerValue);
}
return $valueList;
case 'structValue':
return $this->unpackStructFromApi($value['structValue']);
default:
return $setValue;
}
}
}
private function flattenStruct(array $struct)
{
return $struct['fields'];
}
private function flattenValue(array $value)
{
if (count($value) > 1) {
throw new \RuntimeException("Unexpected fields in struct: $value");
}
if (isset($value['nullValue'])) {
return null;
}
return array_pop($value);
}
private function flattenListValue(array $value)
{
return $value['values'];
}
/**
* Format a list for the API.
*
* @param array $list
* @return array
*/
private function formatListForApi(array $list)
{
$values = [];
foreach ($list as $value) {
$values[] = $this->formatValueForApi($value);
}
return ['values' => $values];
}
/**
* Format a value for the API.
*
* @param mixed $value
* @return array
*/
private function formatValueForApi($value)
{
$type = gettype($value);
switch ($type) {
case 'string':
return ['string_value' => $value];
case 'double':
case 'integer':
return ['number_value' => $value];
case 'boolean':
return ['bool_value' => $value];
case 'NULL':
return ['null_value' => NullValue::NULL_VALUE];
case 'array':
if (!empty($value) && $this->isAssoc($value)) {
return ['struct_value' => $this->formatStructForApi($value)];
}
return ['list_value' => $this->formatListForApi($value)];
}
return [];
}
/**
* Format a gRPC timestamp to match the format returned by the REST API.
*
* @param array $timestamp
* @return string
*/
private function formatTimestampFromApi(array $timestamp)
{
$timestamp += [
'seconds' => 0,
'nanos' => 0
];
$dt = $this->createDateTimeFromSeconds($timestamp['seconds']);
return $this->formatTimeAsString($dt, $timestamp['nanos']);
}
/**
* Format a timestamp for the API with nanosecond precision.
*
* @param string $value
* @return array
*/
private function formatTimestampForApi($value)
{
list($dt, $nanos) = $this->parseTimeString($value);
return [
'seconds' => (int) $dt->format('U'),
'nanos' => (int) $nanos
];
}
/**
* Format a duration for the API.
*
* @param string|mixed $value
* @return array
*/
private function formatDurationForApi($value)
{
if (is_string($value)) {
$d = explode('.', trim($value, 's'));
if (count($d) < 2) {
$seconds = $d[0];
$nanos = 0;
} else {
$seconds = (int) $d[0];
$nanos = $this->convertFractionToNanoSeconds($d[1]);
}
} elseif ($value instanceof Duration) {
$d = $value->get();
$seconds = $d['seconds'];
$nanos = $d['nanos'];
}
return [
'seconds' => $seconds,
'nanos' => $nanos
];
}
/**
* Format a duration from the API
*
* @param array $value
* @return string
*/
private function formatDurationFromApi($value): string
{
$seconds = $value['seconds'];
$nanos = str_pad($value['nanos'], 9, 0, STR_PAD_LEFT);
return "{$seconds}.{$nanos}s";
}
/**
* Construct a gapic client. Allows for tests to intercept.
*
* @param string $gapicName
* @param array $config
* @return mixed
*/
protected function constructGapic($gapicName, array $config)
{
return new $gapicName($config);
}
}

211
vendor/google/cloud-core/src/Iam/Iam.php vendored Normal file
View File

@@ -0,0 +1,211 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Iam;
/**
* IAM Manager
*
* This class is not meant to be used directly. It should be accessed
* through other objects which support IAM.
*
* Policies can be created using the {@see \Google\Cloud\Core\Iam\PolicyBuilder}
* to help ensure their validity.
*
* Example:
* ```
* // IAM policies are obtained via resources which implement IAM.
* // In this example, we'll use PubSub topics to demonstrate
* // how IAM policies are managed.
*
* use Google\Cloud\Spanner\SpannerClient;
*
* $spanner = new SpannerClient(['projectId' => 'my-project']);
* $instance = $spanner->instance('my-new-instance');
*
* $iam = $instance->iam();
* ```
*/
class Iam
{
/**
* @var IamConnectionInterface
*/
private $connection;
/**
* @var string
*/
private $resource;
/**
* @var array
*/
private $policy;
/**
* @var array
*/
private $args;
/**
* @var array
*/
private $options;
/**
* @param IamConnectionInterface $connection
* @param string $resource
* @param array $options [optional] {
* Configuration Options
*
* @type string|null $parent The parent request parameter for the policy.
* If set, policy data will be sent as `request.{$parent}`.
* Otherwise, policy will be sent in request root. **Defaults to**
* `policy`.
* @type array $args Arbitrary data to be sent with the request.
* }
* @access private
*/
public function __construct(IamConnectionInterface $connection, $resource, array $options = [])
{
$options += [
'parent' => 'policy',
'args' => []
];
$this->connection = $connection;
$this->resource = $resource;
$this->options = $options;
}
/**
* 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\Core\Iam\Iam::reload()}.
*
* Example:
* ```
* $policy = $iam->policy();
* ```
*
* @param array $options Configuration Options
* @param int $options['requestedPolicyVersion'] Specify the policy version to
* request from the server. Please see
* [policy versioning](https://cloud.google.com/iam/docs/policies#versions)
* for more information.
* @return array An array of policy data
*/
public function policy(array $options = [])
{
if (!$this->policy) {
$this->reload($options);
}
return $this->policy;
}
/**
* Set the IAM policy for this resource.
*
* Bindings with invalid roles, or non-existent members will raise a server
* error.
*
* Example:
* ```
* $oldPolicy = $iam->policy();
* $oldPolicy['bindings'][0]['members'] = 'user:test@example.com';
*
* $policy = $iam->setPolicy($oldPolicy);
* ```
*
* @param array|PolicyBuilder $policy The new policy, as an array or an
* instance of {@see \Google\Cloud\Core\Iam\PolicyBuilder}.
* @param array $options Configuration Options
* @return array An array of policy data
* @throws \InvalidArgumentException If the given policy is not an array or PolicyBuilder.
*/
public function setPolicy($policy, array $options = [])
{
if ($policy instanceof PolicyBuilder) {
$policy = $policy->result();
}
if (!is_array($policy)) {
throw new \InvalidArgumentException('Given policy data must be an array or an instance of PolicyBuilder.');
}
$request = [];
if ($this->options['parent']) {
$parent = $this->options['parent'];
$request[$parent] = $policy;
} else {
$request = $policy;
}
return $this->policy = $this->connection->setPolicy([
'resource' => $this->resource
] + $request + $options + $this->options['args']);
}
/**
* Test if the current user has the given permissions on this resource.
*
* Invalid permissions will raise a BadRequestException.
*
* Example:
* ```
* $allowedPermissions = $iam->testPermissions([
* 'pubsub.topics.publish',
* 'pubsub.topics.attachSubscription'
* ]);
* ```
*
* @param array $permissions A list of permissions to test
* @param array $options Configuration Options
* @return array A subset of $permissions, with only those allowed included.
*/
public function testPermissions(array $permissions, array $options = [])
{
$res = $this->connection->testPermissions([
'permissions' => $permissions,
'resource' => $this->resource
] + $options + $this->options['args']);
return (isset($res['permissions'])) ? $res['permissions'] : [];
}
/**
* Refresh the IAM policy for this resource.
*
* Example:
* ```
* $policy = $iam->reload();
* ```
*
* @param array $options Configuration Options
* @return array An array of policy data
*/
public function reload(array $options = [])
{
return $this->policy = $this->connection->getPolicy([
'resource' => $this->resource
] + $options + $this->options['args']);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Iam;
/**
* An interface defining how wrappers interact with their IAM implementations.
*
* Some services, such as PubSub, have multiple entities in their API which each
* support IAM for access control. Since we use a single implementation for all
* service interaction with a service, IamConnectionInterface is used to proxy
* requests to the correct method on the service connection.
*
* By delegating control of the request to each service, we can reliably offer a
* single entry point for dealing with IAM in a standard way.
*/
interface IamConnectionInterface
{
/**
* @param array $args
*/
public function getPolicy(array $args);
/**
* @param array $args
*/
public function setPolicy(array $args);
/**
* @param array $args
*/
public function testPermissions(array $args);
}

View File

@@ -0,0 +1,231 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Iam;
use Google\ApiCore\Serializer;
use Google\Cloud\Core\ArrayTrait;
use Google\Cloud\Core\RequestHandler;
use Google\Cloud\Iam\V1\GetIamPolicyRequest;
use Google\Cloud\Iam\V1\GetPolicyOptions;
use Google\Cloud\Iam\V1\Policy;
use Google\Cloud\Iam\V1\SetIamPolicyRequest;
use Google\Cloud\Iam\V1\TestIamPermissionsRequest;
use InvalidArgumentException;
/**
* IAM Manager
*
* This class is not meant to be used directly. It should be accessed
* through other objects which support IAM.
*
* Policies can be created using the {@see PolicyBuilder}
* to help ensure their validity.
*
* Example:
* ```
* // IAM policies are obtained via resources which implement IAM.
* // In this example, we'll use PubSub topics to demonstrate
* // how IAM policies are managed.
*
* use Google\Cloud\PubSub\PubSubClient;
*
* $pubsub = new PubSubClient();
* $topic = $pubsub->topic('my-new-topic');
*
* $iam = $topic->iam();
* ```
*
* @internal
*/
class IamManager
{
use ArrayTrait;
private RequestHandler $requestHandler;
private Serializer $serializer;
private string $clientClass;
private string $resource;
private ?array $policy;
/**
* @param RequestHandler $requestHandler
* @param Serializer $serializer The serializer instance to encode/decode messages.
* @param string $clientClass The client class that will be passed on to the
* sendRequest method of the $requestHandler.
* @param string $resource
* @access private
*/
public function __construct(
RequestHandler $requestHandler,
Serializer $serializer,
string $clientClass,
string $resource
) {
$this->requestHandler = $requestHandler;
$this->serializer = $serializer;
$this->clientClass = $clientClass;
$this->resource = $resource;
$this->policy = null;
}
/**
* 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 IamManager::reload()}.
*
* Example:
* ```
* $policy = $iam->policy();
* ```
*
* @param array $options Configuration Options
* @param int $options['requestedPolicyVersion'] Specify the policy version to
* request from the server. Please see
* [policy versioning](https://cloud.google.com/iam/docs/policies#versions)
* for more information.
* @return array An array of policy data
*/
public function policy(array $options = [])
{
if (!$this->policy) {
$this->reload($options);
}
return $this->policy;
}
/**
* Set the IAM policy for this resource.
*
* Bindings with invalid roles, or non-existent members will raise a server
* error.
*
* Example:
* ```
* $policy = [
* 'bindings' => [[
* 'role' => 'roles/editor',
* 'members' => ['user:test@example.com'],
* ]]
* ];
* $policy = $iam->setPolicy($policy);
* ```
* ```
* $oldPolicy = $iam->policy();
* $oldPolicy['bindings'][0]['members'] = 'user:test@example.com';
*
* $policy = $iam->setPolicy($oldPolicy);
* ```
*
* @param array|PolicyBuilder $policy The new policy, as an array or an
* instance of {@see PolicyBuilder}.
* @param array $options Configuration Options
* @return array An array of policy data
* @throws InvalidArgumentException If the given policy is not an array or PolicyBuilder.
*/
public function setPolicy($policy, array $options = [])
{
if ($policy instanceof PolicyBuilder) {
$policy = $policy->result();
}
if (!is_array($policy)) {
throw new InvalidArgumentException('Given policy data must be an array or an instance of PolicyBuilder.');
}
$policy = $this->serializer->decodeMessage(
new Policy(),
$policy
);
$updateMask = $options['updateMask'] ?? [];
$data = ['resource' => $this->resource, 'policy' => $policy, 'updateMask' => $updateMask];
$request = $this->serializer->decodeMessage(new SetIamPolicyRequest(), $data);
$this->policy = $this->requestHandler->sendRequest(
$this->clientClass,
'setIamPolicy',
$request,
$options
);
return $this->policy;
}
/**
* Test if the current user has the given permissions on this resource.
*
* Invalid permissions will raise a BadRequestException.
*
* Example:
* ```
* $allowedPermissions = $iam->testPermissions([
* 'pubsub.topics.publish',
* 'pubsub.topics.attachSubscription'
* ]);
* ```
*
* @param array $permissions A list of permissions to test
* @param array $options Configuration Options
* @return array A subset of $permissions, with only those allowed included.
*/
public function testPermissions(array $permissions, array $options = [])
{
$data = ['resource' => $this->resource, 'permissions' => $permissions];
$request = $this->serializer->decodeMessage(new TestIamPermissionsRequest(), $data);
$res = $this->requestHandler->sendRequest(
$this->clientClass,
'testIamPermissions',
$request,
$options
);
return isset($res['permissions']) ? $res['permissions'] : [];
}
/**
* Refresh the IAM policy for this resource.
*
* Example:
* ```
* $policy = $iam->reload();
* ```
*
* @param array $options Configuration Options
* @return array An array of policy data
*/
public function reload(array $options = [])
{
$policyOptions = $this->pluck('policyOptions', $options, false) ?: [];
$policyOptions = $this->serializer->decodeMessage(new GetPolicyOptions(), $policyOptions);
$data = ['resource' => $this->resource, 'options' => $policyOptions];
$request = $this->serializer->decodeMessage(new GetIamPolicyRequest(), $data);
$this->policy = $this->requestHandler->sendRequest(
$this->clientClass,
'getIamPolicy',
$request,
$options
);
return $this->policy;
}
}

View File

@@ -0,0 +1,307 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Iam;
use BadMethodCallException;
use InvalidArgumentException;
/**
* Helper class for creating valid IAM policies
*
* Example:
* ```
* use Google\Cloud\Core\Iam\PolicyBuilder;
*
* $builder = new PolicyBuilder();
* $builder->addBinding('roles/admin', [ 'user:admin@domain.com' ]);
* $result = $builder->result();
* ```
*/
class PolicyBuilder
{
/**
* @var array
*/
private $bindings;
/**
* @var string
*/
private $etag;
/**
* @var int
*/
private $version;
/**
* Create a PolicyBuilder.
*
* To use conditions in the bindings, the version of the policy must be set
* to 3.
*
* @see https://cloud.google.com/iam/docs/policies#versions Policy versioning
* @see https://cloud-dot-devsite.googleplex.com/storage/docs/access-control/using-iam-permissions#conditions-iam
* Using Cloud IAM Conditions on buckets
*
* Example:
* ```
* $policy = [
* 'etag' => 'AgIc==',
* 'version' => 3,
* 'bindings' => [
* [
* 'role' => 'roles/admin',
* 'members' => [
* 'user:admin@domain.com',
* 'user2:admin@domain.com'
* ],
* 'condition' => [
* 'title' => 'match-prefix',
* 'description' => 'Applies to objects matching a prefix',
* 'expression' =>
* 'resource.name.startsWith("projects/_/buckets/bucket-name/objects/prefix-a-")'
* ]
* ]
* ],
* ];
*
* $builder = new PolicyBuilder($policy);
* ```
*
* @param array $policy A policy array
* @throws \InvalidArgumentException
*/
public function __construct(array $policy = [])
{
if (isset($policy['bindings'])) {
$this->setBindings($policy['bindings']);
} elseif (!empty($policy)) {
throw new InvalidArgumentException('Invalid Policy');
}
if (isset($policy['etag'])) {
$this->setEtag($policy['etag']);
}
if (isset($policy['version'])) {
$this->setVersion($policy['version']);
}
}
/**
* Override all stored bindings on the policy.
*
* Example:
* ```
* $builder->setBindings([
* [
* 'role' => 'roles/admin',
* 'members' => [
* 'user:admin@domain.com'
* ],
* 'condition' => [
* 'expression' =>
* 'request.time < timestamp("2020-07-01T00:00:00.000Z")'
* ]
* ]
* ]);
* ```
*
* @param array $bindings [optional] An array of bindings
* @return PolicyBuilder
* @throws \InvalidArgumentException
*/
public function setBindings(array $bindings = [])
{
$this->bindings = $bindings;
return $this;
}
/**
* Add a new binding to the policy.
*
* This method will fail with an InvalidOpereationException if it is
* called on a Policy with a version greater than 1 as that indicates
* a more complicated policy than this method is prepared to handle.
* Changes to such policies must be made manually by the setBindings()
* method.
*
*
* Example:
* ```
* $builder->addBinding('roles/admin', [ 'user:admin@domain.com' ]);
* ```
*
* @param string $role A valid role for the service
* @param array $members An array of members to assign to the binding
* @return PolicyBuilder
* @throws \InvalidArgumentException
* @throws BadMethodCallException if the policy's version is greater than 1.
* @deprecated
*/
public function addBinding($role, array $members)
{
$this->validatePolicyVersion();
$this->bindings[] = [
'role' => $role,
'members' => $members
];
return $this;
}
/**
* Remove a binding from the policy.
*
* This method will fail with a BadMethodCallException if it is
* called on a Policy with a version greater than 1 as that indicates
* a more complicated policy than this method is prepared to handle.
* Changes to such policies must be made manually by the setBindings()
* method.
*
* Example:
* ```
* $builder->setBindings([
* [
* 'role' => 'roles/admin',
* 'members' => [
* 'user:admin@domain.com',
* 'user2:admin@domain.com'
* ]
* ]
* ]);
* $builder->removeBinding('roles/admin', [ 'user:admin@domain.com' ]);
* ```
*
* @param string $role A valid role for the service
* @param array $members An array of members to remove from the role
* @return PolicyBuilder
* @throws \InvalidArgumentException
* @throws BadMethodCallException if the policy's version is greater than 1.
* @deprecated
*/
public function removeBinding($role, array $members)
{
$this->validatePolicyVersion();
$bindings = $this->bindings;
foreach ((array) $bindings as $i => $binding) {
if ($binding['role'] == $role) {
$newMembers = array_diff($binding['members'], $members);
if (count($newMembers) != count($binding['members']) - count($members)) {
throw new InvalidArgumentException('One or more role-members were not found.');
}
if (empty($newMembers)) {
unset($bindings[$i]);
$bindings = array_values($bindings);
} else {
$binding['members'] = array_values($newMembers);
$bindings[$i] = $binding;
}
$this->bindings = $bindings;
return $this;
}
}
throw new InvalidArgumentException('The role was not found.');
}
/**
* Update the etag on the policy.
*
* Example:
* ```
* $builder->setEtag($oldPolicy['etag']);
* ```
*
* @param string $etag used for optimistic concurrency control as a way to help prevent simultaneous updates of a
* policy from overwriting each other. It is strongly suggested that updates to existing policies make use
* of the etag to avoid race conditions.
* @return PolicyBuilder
*/
public function setEtag($etag)
{
$this->etag = $etag;
return $this;
}
/**
* Update the version of the policy.
*
* Example:
* ```
* $builder->setVersion(1);
* ```
*
* @param int $version Version of the Policy. **Defaults to** `0`.
* @return PolicyBuilder
*/
public function setVersion($version)
{
$this->version = $version;
return $this;
}
/**
* Create a policy array with data in the correct format.
*
* Example:
* ```
* $policy = $builder->result();
* ```
*
* @return array An array of policy data
*/
public function result()
{
return array_filter([
'etag' => $this->etag,
'bindings' => $this->bindings,
'version' => $this->version
]);
}
private function validatePolicyVersion()
{
if (isset($this->version) && $this->version > 1) {
throw new BadMethodCallException('Helper methods cannot be ' .
"invoked on policies with version {$this->version}.");
}
$this->validateConditions();
}
private function validateConditions()
{
if (!$this->bindings) {
return;
}
foreach ($this->bindings as $binding) {
if (isset($binding['condition'])) {
throw new BadMethodCallException('Helper methods cannot ' .
'be invoked on policies containing conditions.');
}
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Copyright 2022 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\ApiCore\InsecureCredentialsWrapper as ApiCoreInsecureCredentialsWrapper;
/**
* For connect to emulator.
*
* This class is deprecated. Use Google\ApiCore\InsecureCredentialsWrapper instead.
* @deprecated
*/
class InsecureCredentialsWrapper extends ApiCoreInsecureCredentialsWrapper
{
public function __construct()
{
}
}

80
vendor/google/cloud-core/src/Int64.php vendored Normal file
View File

@@ -0,0 +1,80 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Represents a 64 bit integer. This can be useful when working on a 32 bit
* platform.
*
* Example:
* ```
* $int64 = new Int64('9223372036854775807');
* ```
*/
class Int64 implements \JsonSerializable
{
/**
* @var string
*/
private $value;
/**
* @param string $value The 64 bit integer value in string format.
*/
public function __construct($value)
{
$this->value = $value;
}
/**
* Get the value.
*
* Example:
* ```
* $value = $int64->get();
* ```
*
* @return string
*/
public function get()
{
return $this->value;
}
/**
* Provides a convenient way to access the value.
*
* @access private
*/
public function __toString()
{
return $this->value;
}
/**
* Implement JsonSerializable by returning the 64 bit integer as a string
*
* @return string
* @access private
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->value;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Iterator;
/**
* Iterates over a set of items.
*
* @template T
* @implements \Iterator<int, T>
*/
class ItemIterator implements \Iterator
{
use ItemIteratorTrait;
}

View File

@@ -0,0 +1,155 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Iterator;
/**
* This trait fulfills the
* [\Iterator](http://php.net/manual/en/class.iterator.php) interface and
* returns results from a paged set one at a time.
*/
trait ItemIteratorTrait
{
/**
* @var \Iterator
*/
private $pageIterator;
/**
* @var int
*/
private $pageIndex = 0;
/**
* @var int
*/
private $position = 0;
/**
* @param \Iterator $pageIterator
*/
public function __construct(\Iterator $pageIterator)
{
$this->pageIterator = $pageIterator;
}
/**
* Fetch the token used to get the next set of results.
*
* @return string|null
*/
public function nextResultToken()
{
return method_exists($this->pageIterator, 'nextResultToken')
? $this->pageIterator->nextResultToken()
: null;
}
/**
* Iterate over the results on a per page basis.
*
* @return \Iterator
*/
public function iterateByPage()
{
return $this->pageIterator;
}
/**
* Rewind the iterator.
*
* @return null
*/
#[\ReturnTypeWillChange]
public function rewind()
{
$this->pageIndex = 0;
$this->position = 0;
$this->pageIterator->rewind();
}
/**
* Get the current item.
*
* @return mixed
*/
#[\ReturnTypeWillChange]
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
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->position;
}
/**
* Advances to the next item.
*
* @return null
*/
#[\ReturnTypeWillChange]
public function next()
{
$this->pageIndex++;
$this->position++;
if (count($this->pageIterator->current()) <= $this->pageIndex && $this->nextResultToken()) {
$this->pageIterator->next();
$this->pageIndex = 0;
}
}
/**
* Determines if the current position is valid.
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function valid()
{
$page = $this->pageIterator->current();
if (isset($page[$this->pageIndex])) {
return true;
}
// If there are no results, but a token for the next page
// exists let's continue paging until there are results.
while ($this->nextResultToken()) {
$this->pageIterator->next();
$page = $this->pageIterator->current();
if (isset($page[$this->pageIndex])) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Iterator;
/**
* Iterates over a set of pages.
*/
class PageIterator implements \Iterator
{
use PageIteratorTrait;
}

View File

@@ -0,0 +1,326 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Iterator;
use Google\Cloud\Core\ArrayTrait;
/**
* This trait fulfills the
* [\Iterator](http://php.net/manual/en/class.iterator.php) interface and
* returns results as a page of items.
*/
trait PageIteratorTrait
{
use ArrayTrait;
/**
* @var array|null
*/
private $page;
/**
* @var callable
*/
private $resultMapper;
/**
* @var callable
*/
private $call;
/**
* @var array
*/
private $callOptions;
/**
* @var array
*/
private $config;
/**
* @var int
*/
private $position = 0;
/**
* @var int
*/
private $itemCount = 0;
/**
* @var array
*/
private $resultTokenPath;
/**
* @var array
*/
private $nextResultTokenPath;
/**
* @var array
*/
private $itemsPath;
/**
* @var string|null
*/
private $initialResultToken;
/**
* @param callable $resultMapper Maps a result.
* @param callable $call The call to execute.
* @param array $callOptions Options to use with the call.
* @param array $config [optional] {
* Configuration options.
*
* @type string $itemsKey The key for the items to iterate over from the
* response. **Defaults to** `"items"`.
* @type string $nextResultTokenKey The key for the next result token in
* the response. **Defaults to** `"nextPageToken"`.
* @type string $resultTokenKey The key for the results token set in the
* request. **Defaults too** `"pageToken"`.
* @type array $firstPage The first page of results. If set, this data
* will be used for the first page of results instead of making
* a network request.
* @type callable $setNextResultTokenCondition If this condition passes
* then it should be considered safe to set the token to get the
* next set of results.
* @type int $resultLimit Limit the number of results returned in total.
* **Defaults to** `0` (return all results).
* }
*/
public function __construct(
callable $resultMapper,
callable $call,
array $callOptions,
array $config = []
) {
$this->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
*/
#[\ReturnTypeWillChange]
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
*/
#[\ReturnTypeWillChange]
public function current()
{
if ($this->page === null) {
$this->page = $this->executeCall();
}
$page = $this->get($this->itemsPath, $this->page);
if ($this->nextResultToken()) {
return $page ?: [];
}
return $page;
}
/**
* Get the key current page's key.
*
* @return int
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->position;
}
/**
* Advances to the next page.
*
* @return null
*/
#[\ReturnTypeWillChange]
public function next()
{
$this->position++;
$this->page = $this->nextResultToken()
? $this->executeCall()
: null;
}
/**
* Determines if the current position is valid.
*
* @return bool
*/
#[\ReturnTypeWillChange]
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)
{
$result = $array;
foreach ($path as $key) {
if (!isset($result[$key])) {
return null;
}
$result = $result[$key];
}
return $result;
}
/**
* @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;
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Provides wrappers for json_encode/json_decode that throw exceptions when an
* error is encountered.
*/
trait JsonTrait
{
/**
* @param string $json The json string being decoded.
* @param bool $assoc When true, returned objects will be converted into
* associative arrays.
* @param int $depth User specified recursion depth.
* @param int $options Bitmask of JSON decode options.
* @return mixed
* @throws \InvalidArgumentException
*/
private static function jsonDecode($json, $assoc = false, $depth = 512, $options = 0)
{
$data = json_decode($json, $assoc, $depth, $options);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException(
'json_decode error: ' . json_last_error_msg()
);
}
return $data;
}
/**
* @param mixed $value The value being encoded. Can be any type except a
* resource.
* @param int $options Bitmask of JSON encode options.
* @param int $depth Set the maximum depth. Must be greater than zero.
* @return string
* @throws \InvalidArgumentException
*/
private static function jsonEncode($value, $options = 0, $depth = 512)
{
$json = json_encode($value, $options, $depth);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException(
'json_encode error: ' . json_last_error_msg()
);
}
return $json;
}
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\Lock;
/**
* Flock based lock implementation.
*
* @see http://php.net/manual/en/function.flock.php
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class FlockLock implements LockInterface
{
use LockTrait;
const FILE_PATH_TEMPLATE = '%s/%s.lock';
/**
* @var string
*/
private $filePath;
/**
* @var resource|null
*/
private $handle;
/**
* @var bool If true, we should acquire an exclusive lock.
*/
private $exclusive;
/**
* @param string $fileName The name of the file to use as a lock.
* @param array $options [optional] {
* Configuration options.
*
* @type bool $exclusive If true, acquire an excluse (write) lock. If
* false, acquire a shared (read) lock. **Defaults to** true.
* }
* @throws \InvalidArgumentException If an invalid fileName is provided.
*/
public function __construct($fileName, array $options = [])
{
if (!is_string($fileName)) {
throw new \InvalidArgumentException('$fileName must be a string.');
}
$options += [
'exclusive' => true
];
$this->exclusive = $options['exclusive'];
$this->filePath = sprintf(
self::FILE_PATH_TEMPLATE,
sys_get_temp_dir(),
$fileName
);
}
/**
* Acquires a lock that will block until released.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException If the lock fails to be acquired.
*/
public function acquire(array $options = [])
{
if ($this->handle) {
return true;
}
$this->handle = $this->initializeHandle();
if (!flock($this->handle, $this->lockType($options))) {
fclose($this->handle);
$this->handle = null;
throw new \RuntimeException('Failed to acquire lock.');
}
return true;
}
/**
* Releases the lock.
*
* @throws \RuntimeException If the lock fails to release.
*/
public function release()
{
if ($this->handle) {
$released = flock($this->handle, LOCK_UN);
fclose($this->handle);
$this->handle = null;
if (!$released) {
throw new \RuntimeException('Failed to release lock.');
}
}
}
/**
* Initializes the handle.
*
* @return resource
* @throws \RuntimeException If the lock file fails to open.
*/
private function initializeHandle()
{
$handle = @fopen($this->filePath, 'c');
if (!$handle) {
throw new \RuntimeException('Failed to open lock file.');
}
return $handle;
}
private function lockType(array $options)
{
$options += [
'blocking' => true
];
$lockType = $this->exclusive ? LOCK_EX : LOCK_SH;
if (!$options['blocking']) {
$lockType |= LOCK_UN;
}
return $lockType;
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\Lock;
/**
* Contract for a basic locking mechanism.
*/
interface LockInterface
{
/**
* Acquires a lock.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException If the lock fails to be acquired.
*/
public function acquire(array $options = []);
/**
* Releases the lock.
*
* @throws \RuntimeException
*/
public function release();
/**
* Execute a callable within a lock.
*
* @param callable $func The callable to execute.
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return mixed
*/
public function synchronize(callable $func, array $options = []);
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\Lock;
/**
* Utility trait for locks.
*/
trait LockTrait
{
/**
* Acquires a lock that will block until released.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException If the lock fails to be acquired.
*/
abstract public function acquire(array $options = []);
/**
* Releases the lock.
*
* @throws \RuntimeException
*/
abstract public function release();
/**
* Execute a callable within a lock. If an exception is caught during
* execution of the callable the lock will first be released before throwing
* it.
*
* @param callable $func The callable to execute.
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return mixed
*/
public function synchronize(callable $func, array $options = [])
{
$result = null;
$exception = null;
if ($this->acquire($options)) {
try {
$result = $func();
} catch (\Exception $ex) {
$exception = $ex;
}
$this->release();
}
if ($exception) {
throw $exception;
}
return $result;
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\Lock;
use Google\Cloud\Core\SysvTrait;
/**
* Semaphore based lock implementation.
*
* @see http://php.net/manual/en/book.sem.php
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class SemaphoreLock implements LockInterface
{
use LockTrait;
use SysvTrait;
/**
* @var int
*/
private $key;
/**
* @var resource|null
*/
private $semaphoreId;
/**
* @param int $key A key.
* @throws \InvalidArgumentException If an invalid key is provided.
* @throws \RuntimeException If the System V IPC extensions are missing.
*/
public function __construct($key)
{
if (!$this->isSysvIPCLoaded()) {
throw new \RuntimeException('SystemV IPC extensions are required.');
}
if (!is_int($key)) {
throw new \InvalidArgumentException('The provided key must be an integer.');
}
$this->key = $key;
}
/**
* Acquires a lock that will block until released.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException If the lock fails to be acquired.
*/
public function acquire(array $options = [])
{
$options += [
'blocking' => true
];
if ($this->semaphoreId) {
return true;
}
$this->semaphoreId = $this->initializeId();
if (!sem_acquire($this->semaphoreId, !$options['blocking'])) {
$this->semaphoreId = null;
throw new \RuntimeException('Failed to acquire lock.');
}
return true;
}
/**
* Releases the lock.
*
* @throws \RuntimeException If the lock fails to release.
*/
public function release()
{
if ($this->semaphoreId) {
$released = sem_release($this->semaphoreId);
$this->semaphoreId = null;
if (!$released) {
throw new \RuntimeException('Failed to release lock.');
}
}
}
/**
* Initializes the semaphore ID.
*
* @return resource
* @throws \RuntimeException If semaphore ID fails to generate.
*/
private function initializeId()
{
$semaphoreId = sem_get($this->key);
if (!$semaphoreId) {
throw new \RuntimeException('Failed to generate semaphore ID.');
}
return $semaphoreId;
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\Lock;
use Symfony\Component\Lock\LockInterface as SymfonyLockInterface;
/**
* Symfony lock component adapter.
* @deprecated
*/
class SymfonyLockAdapter implements LockInterface
{
use LockTrait;
/**
* @var SymfonyLockInterface
*/
private $lock;
/**
* @param SymfonyLockInterface $lock
*/
public function __construct(SymfonyLockInterface $lock)
{
$this->lock = $lock;
}
/**
* Acquires a lock that will block until released.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException If the lock fails to be acquired.
*/
public function acquire(array $options = [])
{
$options += [
'blocking' => true
];
try {
return $this->lock->acquire($options['blocking']);
} catch (\Exception $ex) {
throw new \RuntimeException(
sprintf(
'Acquiring the lock failed with the following message: %s',
$ex->getMessage()
),
0,
$ex
);
}
}
/**
* Releases the lock.
*
* @throws \RuntimeException
*/
public function release()
{
try {
$this->lock->release();
} catch (\Exception $ex) {
throw new \RuntimeException(
sprintf(
'Releasing the lock failed with the following message: %s',
$ex->getMessage()
),
0,
$ex
);
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Logger;
use Monolog\Formatter\LineFormatter;
/**
* Monolog 1.x formatter for formatting logs on App Engine flexible environment.
*
* If you are using Monolog 2.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexFormatterV2} instead.
* If you are using Monolog 3.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexFormatterV3} instead.
*/
class AppEngineFlexFormatter extends LineFormatter
{
use FormatterTrait;
/**
* @param string $format [optional] The format of the message
* @param string $dateFormat [optional] The format of the timestamp
* @param bool $ignoreEmptyContextAndExtra [optional]
*/
public function __construct($format = null, $dateFormat = null, $ignoreEmptyContextAndExtra = false)
{
parent::__construct($format, $dateFormat, true, $ignoreEmptyContextAndExtra);
}
/**
* Get the plain text message with LineFormatter's format method and add
* metadata including the trace id then return the json string.
*
* @param array $record A record to format
* @return string The formatted record
*/
public function format(array $record)
{
return $this->formatPayload($record, parent::format($record));
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* Copyright 2019 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Logger;
use Monolog\Formatter\LineFormatter;
/**
* Monolog 2.x formatter for formatting logs on App Engine flexible environment.
*
* If you are using Monolog 1.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexFormatter} instead.
* If you are using Monolog 3.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexFormatterV3} instead.
*/
class AppEngineFlexFormatterV2 extends LineFormatter
{
use FormatterTrait;
/**
* @param string $format [optional] The format of the message
* @param string $dateFormat [optional] The format of the timestamp
* @param bool $ignoreEmptyContextAndExtra [optional]
*/
public function __construct($format = null, $dateFormat = null, $ignoreEmptyContextAndExtra = false)
{
parent::__construct($format, $dateFormat, true, $ignoreEmptyContextAndExtra);
}
/**
* Get the plain text message with LineFormatter's format method and add
* metadata including the trace id then return the json string.
*
* @param array $record A record to format
* @return string The formatted record
*/
public function format(array $record): string
{
return $this->formatPayload($record, parent::format($record));
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* Copyright 2022 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Logger;
use Monolog\Formatter\LineFormatter;
use Monolog\LogRecord;
/**
* Monolog 3.x formatter for formatting logs on App Engine flexible environment.
*
* If you are using Monolog 1.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexFormatter} instead.
* If you are using Monolog 2.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexFormatterV2} instead.
*/
class AppEngineFlexFormatterV3 extends LineFormatter
{
use FormatterTrait;
/**
* @param string $format [optional] The format of the message
* @param string $dateFormat [optional] The format of the timestamp
* @param bool $ignoreEmptyContextAndExtra [optional]
*/
public function __construct($format = null, $dateFormat = null, $ignoreEmptyContextAndExtra = false)
{
parent::__construct($format, $dateFormat, true, $ignoreEmptyContextAndExtra);
}
/**
* Get the plain text message with LineFormatter's format method and add
* metadata including the trace id then return the json string.
*
* @param LogRecord $record A record to format
* @return string The formatted record
*/
public function format(LogRecord $record): string
{
return $this->formatPayload($record, parent::format($record));
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
/**
* Monolog 1.x handler for logging on App Engine flexible environment.
*
* If you are using Monolog 2.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexHandlerV2} instead.
* If you are using Monolog 3.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexHandlerV3} instead.
* @internal
*/
class AppEngineFlexHandler extends StreamHandler
{
/**
* @param int $level [optional] The minimum logging level at which this
* handler will be triggered.
* @param Boolean $bubble [optional] Whether the messages that are handled
* can bubble up the stack or not.
* @param int|null $filePermission [optional] Optional file permissions
* (default (0640) are only for owner read/write).
* @param Boolean $useLocking [optional] Try to lock log file before doing
* any writes.
* @param resource|string|null $stream [optional]
*/
public function __construct(
$level = Logger::INFO,
$bubble = true,
$filePermission = 0640,
$useLocking = false,
$stream = null
) {
if ($stream === null) {
$pid = getmypid();
$stream = "file:///var/log/app_engine/app.$pid.json";
}
parent::__construct(
$stream,
$level,
$bubble,
$filePermission,
$useLocking
);
}
/**
* Get the default formatter.
*
* @return FormatterInterface
*/
protected function getDefaultFormatter()
{
return new AppEngineFlexFormatter();
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* Copyright 2019 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Logger;
use Exception;
use Monolog\Logger;
/**
* Factory to build out an AppEngineFlexHandler for the installed version of Monolog.
*/
class AppEngineFlexHandlerFactory
{
/**
* Builds out an AppEngineFlexHandler for the installed version of Monolog.
*
* @param int $level [optional] The minimum logging level at which this
* handler will be triggered.
* @param Boolean $bubble [optional] Whether the messages that are handled
* can bubble up the stack or not.
* @param int|null $filePermission [optional] Optional file permissions
* (default (0640) are only for owner read/write).
* @param Boolean $useLocking [optional] Try to lock log file before doing
* any writes.
* @param resource|string|null $stream [optional]
*
* @throws Exception
*
* @return AppEngineFlexHandler|AppEngineFlexHandlerV2|AppEngineFlexHandlerV3
*/
public static function build(
$level = Logger::INFO,
$bubble = true,
$filePermission = 0640,
$useLocking = false,
$stream = null
) {
$version = defined('Monolog\Logger::API') ? Logger::API : 1;
switch ($version) {
case 1:
return new AppEngineFlexHandler($level, $bubble, $filePermission, $useLocking, $stream);
case 2:
return new AppEngineFlexHandlerV2($level, $bubble, $filePermission, $useLocking, $stream);
case 3:
return new AppEngineFlexHandlerV3($level, $bubble, $filePermission, $useLocking, $stream);
default:
throw new Exception('Version not supported');
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* Copyright 2019 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
/**
* Monolog 2.x handler for logging on App Engine flexible environment.
*
* If you are using Monolog 1.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexHandler} instead.
* If you are using Monolog 3.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexHandlerV3} instead.
* @internal
*/
class AppEngineFlexHandlerV2 extends StreamHandler
{
/**
* @param int $level [optional] The minimum logging level at which this
* handler will be triggered.
* @param Boolean $bubble [optional] Whether the messages that are handled
* can bubble up the stack or not.
* @param int|null $filePermission [optional] Optional file permissions
* (default (0640) are only for owner read/write).
* @param Boolean $useLocking [optional] Try to lock log file before doing
* any writes.
* @param resource|string|null $stream [optional]
*/
public function __construct(
$level = Logger::INFO,
$bubble = true,
$filePermission = 0640,
$useLocking = false,
$stream = null
) {
if ($stream === null) {
$pid = getmypid();
$stream = "file:///var/log/app_engine/app.$pid.json";
}
parent::__construct(
$stream,
$level,
$bubble,
$filePermission,
$useLocking
);
}
protected function getDefaultFormatter(): FormatterInterface
{
return new AppEngineFlexFormatterV2();
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* Copyright 2022 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
/**
* Monolog 3.x handler for logging on App Engine flexible environment.
*
* If you are using Monolog 1.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexHandler} instead.
* If you are using Monolog 2.x, use {@see \Google\Cloud\Core\Logger\AppEngineFlexHandlerV2} instead.
* @internal
*/
class AppEngineFlexHandlerV3 extends StreamHandler
{
/**
* @param int $level [optional] The minimum logging level at which this
* handler will be triggered.
* @param Boolean $bubble [optional] Whether the messages that are handled
* can bubble up the stack or not.
* @param int|null $filePermission [optional] Optional file permissions
* (default (0640) are only for owner read/write).
* @param Boolean $useLocking [optional] Try to lock log file before doing
* any writes.
* @param resource|string|null $stream [optional]
*/
public function __construct(
$level = Logger::INFO,
$bubble = true,
$filePermission = 0640,
$useLocking = false,
$stream = null
) {
if ($stream === null) {
$pid = getmypid();
$stream = "file:///var/log/app_engine/app.$pid.json";
}
parent::__construct(
$stream,
$level,
$bubble,
$filePermission,
$useLocking
);
}
protected function getDefaultFormatter(): FormatterInterface
{
return new AppEngineFlexFormatterV3();
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* Copyright 2019 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Logger;
use Monolog\LogRecord;
/**
* Shared trait to enrich and format a record with
* App Engine Flex specific information.
*/
trait FormatterTrait
{
/**
* @param array|LogRecord $record
* @param string $message
* @return string
*/
protected function formatPayload($record, $message)
{
if ($record instanceof LogRecord) {
$record = $record->toArray();
}
list($usec, $sec) = explode(' ', microtime());
$usec = (int) (((float) $usec) * 1000000000);
$sec = (int) $sec;
$payload = [
'message' => $message,
'timestamp' => ['seconds' => $sec, 'nanos' => $usec],
'thread' => '',
'severity' => $record['level_name'],
];
if (isset($_SERVER['HTTP_X_CLOUD_TRACE_CONTEXT'])) {
$payload['traceId'] = explode(
'/',
$_SERVER['HTTP_X_CLOUD_TRACE_CONTEXT']
)[0];
}
return "\n" . json_encode($payload);
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* Copyright 2016 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\LongRunning;
use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\Core\Iterator\PageIterator;
/**
* Provide Long Running Operation support to Google Cloud PHP Clients.
*
* This trait should be used by a user-facing client which implements LRO.
*/
trait LROTrait
{
/**
* @var LongRunningConnectionInterface
* @internal
*/
private $lroConnection;
/**
* @var array
*/
private $lroCallables;
/**
* @var string
*/
private $lroResource;
/**
* Populate required LRO properties.
*
* @param LongRunningConnectionInterface $lroConnection The LRO Connection.
* This object is created by internal classes,
* and should not be instantiated outside of this context.
* @param array $callablesMap An collection of form [(string) typeUrl, (callable) callable]
* providing a function to invoke when an operation completes. The
* callable Type should correspond to an expected value of
* operation.metadata.typeUrl.
* @param string $lroResource [optional] The resource for which operations
* may be listed.
*/
private function setLroProperties(
LongRunningConnectionInterface $lroConnection,
array $lroCallables,
$resource = null
) {
$this->lroConnection = $lroConnection;
$this->lroCallables = $lroCallables;
$this->lroResource = $resource;
}
/**
* Resume a Long Running Operation
*
* @param string $operationName The Long Running Operation name.
* @param array $info [optional] The operation data.
* @return LongRunningOperation
*/
public function resumeOperation($operationName, array $info = [])
{
return new LongRunningOperation(
$this->lroConnection,
$operationName,
$this->lroCallables,
$info
);
}
/**
* List long running operations.
*
* @param array $options [optional] {
* Configuration Options.
*
* @type string $name The name of the operation collection.
* @type string $filter The standard list filter.
* @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<LongRunningOperation>
*/
public function longRunningOperations(array $options = [])
{
if (is_null($this->lroResource)) {
throw new \BadMethodCallException('This service does list support listing operations.');
}
$resultLimit = $this->pluck('resultLimit', $options, false) ?: 0;
$options['name'] = $this->lroResource . '/operations';
return new ItemIterator(
new PageIterator(
function (array $operation) {
return $this->resumeOperation($operation['name'], $operation);
},
[$this->lroConnection, 'operations'],
$options,
[
'itemsKey' => 'operations',
'resultLimit' => $resultLimit
]
)
);
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* Copyright 2025 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\LongRunning;
use Google\ApiCore\OperationResponse;
use Google\ApiCore\Serializer;
use Google\Cloud\Core\RequestProcessorTrait;
use Google\LongRunning\ListOperationsRequest;
use Google\Protobuf\Any;
/**
* Defines the calls required to manage Long Running Operations using a GAPIC
* generated client.
*
* @internal
*/
class LongRunningClientConnection implements LongRunningConnectionInterface
{
use RequestProcessorTrait;
public function __construct(
private object $gapicClient,
private Serializer $serializer
) {
}
/**
* @param array $args
* @return array
*/
public function get(array $args): array
{
$operationResponse = $this->gapicClient->resumeOperation($args['name']);
return $this->operationResponseToArray($operationResponse);
}
/**
* @param array $args
* @return array
*/
public function cancel(array $args): array
{
$operationResponse = $this->gapicClient->resumeOperation(
$args['name'],
$args['method'] ?? null
);
$operationResponse->cancel();
return $this->operationResponseToArray($operationResponse);
}
/**
* @param array $args
* @return array
*/
public function delete(array $args): array
{
$operationResponse = $this->gapicClient->resumeOperation(
$args['name'],
$args['method'] ?? null
);
$operationResponse->cancel();
return $this->operationResponseToArray($operationResponse);
}
/**
* @param array $args
* @return array
*/
public function operations(array $args): array
{
$request = ListOperationsRequest::build($args['name'], $args['filter'] ?? null);
$response = $this->gapicClient->getOperationsClient()->listOperations($request);
return $this->handleResponse($response);
}
private function operationResponseToArray(OperationResponse $operationResponse): array
{
$response = $this->handleResponse($operationResponse->getLastProtoResponse());
$metaType = $response['metadata']['typeUrl'];
// unpack result Any type
$result = $operationResponse->getResult();
if ($result instanceof Any) {
// For some reason we aren't doing this in GAX OperationResponse (but we should)
$result = $result->unpack();
}
$response['response'] = $this->handleResponse($result);
// unpack error Any type
$response['error'] = $this->handleResponse($operationResponse->getError());
$metadata = $operationResponse->getMetadata();
if ($metadata instanceof Any) {
// For some reason we aren't doing this in GAX OperationResponse (but we should)
$metadata = $metadata->unpack();
}
$response['metadata'] = $this->handleResponse($metadata);
// Used in LongRunningOperation to invoke callables
$response['metadata'] += ['typeUrl' => $metaType];
return $response;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* Copyright 2016 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\LongRunning;
/**
* Defines the calls required to manage Long Running Operations.
*
* This interface should be implemented in a service's Connection namespace.
* @internal
*/
interface LongRunningConnectionInterface
{
/**
* @param array $args
*/
public function get(array $args);
/**
* @param array $args
*/
public function cancel(array $args);
/**
* @param array $args
*/
public function delete(array $args);
/**
* @param array $args
*/
public function operations(array $args);
}

View File

@@ -0,0 +1,384 @@
<?php
/**
* Copyright 2016 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\LongRunning;
/**
* Represent and interact with a Long Running Operation.
* @template T
*/
class LongRunningOperation
{
const WAIT_INTERVAL = 1.0;
const STATE_IN_PROGRESS = 'inProgress';
const STATE_SUCCESS = 'success';
const STATE_ERROR = 'error';
/**
* @var LongRunningConnectionInterface
* @internal
*/
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.
* This object is created by internal classes,
* and should not be instantiated outside of this context.
* @param string $name The Operation name.
* @param array $callablesMap An collection of form [(string) typeUrl, (callable) callable]
* providing a function to invoke when an operation completes. The
* callable Type should correspond to an expected value of
* operation.metadata.typeUrl.
* @param array $info [optional] The operation info.
*/
public function __construct(
LongRunningConnectionInterface $connection,
$name,
array $callablesMap,
array $info = []
) {
$this->connection = $connection;
$this->name = $name;
$this->callablesMap = $callablesMap;
$this->info = $info;
}
/**
* Return the Operation name.
*
* Example:
* ```
* $name = $operation->name();
* ```
*
* @return string
*/
public function name()
{
return $this->name;
}
/**
* Check if the Operation is done.
*
* If the Operation state is not available, a service request may be executed
* by this method.
*
* Example:
* ```
* if ($operation->done()) {
* echo "The operation is done!";
* }
* ```
*
* @param array $options [optional] Configuration options.
* @return bool
*/
public function done(array $options = [])
{
return (isset($this->info($options)['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`.
*
* If the Operation state is not available, a service request may be executed
* by this method.
*
* 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;
* }
* ```
*
* @param array $options [optional] Configuration options.
* @return string
*/
public function state(array $options = [])
{
if (!$this->done($options)) {
return self::STATE_IN_PROGRESS;
}
if ($this->done() && $this->result()) {
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.
*
* If the Operation state is not available, a service request may be executed
* by this method.
*
* Example:
* ```
* $result = $operation->result();
* ```
*
* @param array $options [optional] Configuration options.
* @return T|mixed|null
*/
public function result(array $options = [])
{
$this->info($options);
return $this->result;
}
/**
* Get the Operation error.
*
* Returns null if the Operation is not yet complete, or if no error occurred.
*
* If the Operation state is not available, a service request may be executed
* by this method.
*
* Example:
* ```
* $error = $operation->error();
* ```
*
* @param array $options [optional] Configuration options.
* @return array|null
*/
public function error(array $options = [])
{
$this->info($options);
return $this->error;
}
/**
* Get the Operation info.
*
* If the Operation state is not available, a service request may be executed
* by this method.
*
* Example:
* ```
* $info = $operation->info();
* ```
*
* @codingStandardsIgnoreStart
* @param array $options [optional] 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:
* ```
* $result = $operation->reload();
* ```
*
* @codingStandardsIgnoreStart
* @param array $options [optional] 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);
$this->result = null;
$this->error = null;
if ($res['done'] ?? false && isset($res['metadata']['typeUrl'])) {
$type = $res['metadata']['typeUrl'];
$this->result = $this->executeDoneCallback($type, $res['response']);
$this->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),
'info' => $this->info
];
}
}

View File

@@ -0,0 +1,155 @@
<?php
/**
* Copyright 2016 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core\LongRunning;
use Google\ApiCore\OperationResponse;
use Google\ApiCore\Serializer;
use Google\GAX\OperationResponse as GaxOperationResponse;
/**
* Serializes and deserializes ApiCore LRO Response objects.
*
* This trait should be used in a gRPC Connection class to normalize responses.
*/
trait OperationResponseTrait
{
/**
* Convert a ApiCore OperationResponse object to an array.
*
* @param OperationResponse|GaxOperationResponse $operation The operation response
* @param Serializer|GaxSerializer $serializer The serializer to use for gRPC serialization/deserialization.
* @param array $lroMappers A list of mappers for deserializing operation results.
* @return array
*/
private function operationToArray($operation, $serializer, array $lroMappers)
{
$response = $operation->getLastProtoResponse();
if (is_null($response)) {
return null;
}
$response = $serializer->encodeMessage($response);
$result = null;
if ($operation->isDone() && isset($response['response']['typeUrl'])) {
$type = $response['response']['typeUrl'];
$result = $this->deserializeResult($operation, $type, $serializer, $lroMappers);
}
$metaType = $response['metadata']['typeUrl'];
$metaResult = $this->deserializeMetadata($operation, $metaType, $serializer, $lroMappers);
/** @see LongRunningOperation#reload() */
$metaResult += ['typeUrl' => $metaType];
$error = $operation->getError();
if (!is_null($error)) {
$error = $serializer->encodeMessage($error);
}
$response['response'] = $result;
$response['metadata'] = $metaResult;
$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.
* @param string|null $method The method name.
* @return OperationResponse
*/
private function getOperationByName($client, $name, $method = null)
{
return $client->resumeOperation($name, $method);
}
/**
* Convert an operation response to an array
*
* @param OperationResponse|GaxOperationResponse $operation The operation to
* serialize.
* @param string $type The Operation type. The type should correspond to a
* member of $mappers.typeUrl.
* @param Serializer|GaxSerializer $serializer The gRPC serializer to use
* for the deserialization.
* @param array $mappers A list of mappers.
* @return array|null
*/
private function deserializeResult($operation, $type, $serializer, 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->mergeFromString($anyResponse->getValue());
return $serializer->encodeMessage($response);
}
/**
* Convert an operation metadata to an array
*
* @param OperationResponse|GaxOperationResponse $operation The operation to
* serialize.
* @param string $type The Operation type. The type should correspond to a
* member of $mappers.typeUrl.
* @param Serializer|GaxSerializer $serializer The gRPC serializer to use
* for the deserialization.
* @param array $mappers A list of mappers.
* @return array|null
*/
private function deserializeMetadata($operation, $type, $serializer, 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 metadata type %s.', $type));
}
$mapper = current($mappers);
$message = $mapper['message'];
$response = new $message();
$anyResponse = $operation->getLastProtoResponse()->getMetadata();
if (is_null($anyResponse)) {
return null;
}
$response->mergeFromString($anyResponse->getValue());
return $serializer->encodeMessage($response);
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* Copyright 2025 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\ApiCore\ArrayTrait;
use Google\ApiCore\Options\CallOptions;
use Google\ApiCore\Serializer;
use Google\Protobuf\Internal\Message;
use LogicException;
/**
* Helper used to validate options
*
* @internal
*/
class OptionsValidator
{
use ArrayTrait;
/**
* @param ?Serializer $serializer use a serializer to decode protobuf messages
* instead of calling {@see Message::mergeFromJsonString()}.
*/
public function __construct(
private ?Serializer $serializer = null
) {
}
/**
* Validate an array of options based on the supplied `$optionTypes`.
* $optionTypes can be an array of string keys, a protobuf Message classname, or a
* the CallOptions classname. Parameters are split and returned in the order
* that the options types are provided.
*
* - If the option type is an array, any keys in $options matching the string values
* of the array are returned.
* - If the option type is {@see Message}, any keys matching getters will be set on the message.
* - If the option type is string, and that string is a valid {@see CallOptions} option, those
* options will be returned in an array
*
* ```
* [$customOps, $commitRequest, $callOptions] = $optionsValidator->vaidateOptions(
* $options,
* ['customOp1', 'customOp2'],
* new CommitRequest(),
* CallOptions::class,
* );
* ```
*
* @param array $options
* @param array|Message|string ...$optionTypes
* @return array
* @throws LogicException when a value exists which is not supported by any of the `$optionTypes`.
*/
public function validateOptions(array $options, array|Message|string ...$optionTypes): array
{
$splitOptions = [];
foreach ($optionTypes as $optionType) {
if (is_array($optionType)) {
$splitOptions[] = $this->pluckArray($optionType, $options);
} elseif ($optionType === CallOptions::class) {
$callOptionKeys = array_keys((new CallOptions([]))->toArray());
$splitOptions[] = $this->pluckArray($callOptionKeys, $options);
} elseif ($optionType instanceof Message) {
$messageKeys = array_map(
fn ($method) => lcfirst(substr($method, 3)),
array_filter(
get_class_methods($optionType),
fn ($m) => 0 === strpos($m, 'get')
)
);
$messageOptions = $this->pluckArray($messageKeys, $options);
if ($this->serializer) {
$optionType = $this->serializer->decodeMessage($optionType, $messageOptions);
} else {
$optionType->mergeFromJsonString(json_encode($messageOptions, JSON_FORCE_OBJECT));
}
$splitOptions[] = $optionType;
} elseif (is_string($optionType)) {
$splitOptions[] = $this->pluck($optionType, $options, false);
} else {
throw new LogicException(sprintf('Invalid option type: %s', $optionType));
}
}
if (!empty($options)) {
throw new LogicException(
'Unexpected option(s) provided: ' . implode(', ', array_keys($options))
);
}
return $splitOptions;
}
}

View File

@@ -0,0 +1,239 @@
<?php
/**
* Copyright 2016 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core;
use DrSlump\Protobuf;
use google\protobuf\ListValue;
use google\protobuf\NullValue;
use google\protobuf\Struct;
use google\protobuf\Value;
/**
* Extend the Protobuf-PHP array codec to allow messages to match the format
* used for REST.
* @deprecated
* @codeCoverageIgnore
* @internal
*/
class PhpArray extends Protobuf\Codec\PhpArray
{
/**
* @var array
*/
private $customFilters;
/**
* @var bool
*/
private $useCamelCase;
/**
* @param array $customFilters A set of callbacks to apply to properties in
* a gRPC response.
* @param bool $useCamelCase Whether to convert key casing to camelCase.
* }
*/
public function __construct(array $customFilters = [], $useCamelCase = true)
{
$this->customFilters = $customFilters;
$this->useCamelCase = $useCamelCase;
}
/**
* Borrowed heavily from {@see DrSlump\Protobuf\Codec\PhpArray::encodeMessage()}.
* With this approach we are able to transform the response with minimal
* overhead.
*/
protected function encodeMessage(Protobuf\Message $message)
{
$descriptor = Protobuf::getRegistry()->getDescriptor($message);
$data = [];
foreach ($descriptor->getFields() as $tag => $field) {
$empty = !$message->_has($tag);
if ($field->isRequired() && $empty) {
throw new \UnexpectedValueException(
sprintf(
'Message %s\'s field tag %s(%s) is required but has no value',
get_class($message),
$tag,
$field->getName()
)
);
}
if ($empty) {
continue;
}
$key = $this->useTagNumber ? $field->getNumber() : $field->getName();
$v = $message->_get($tag);
if ($field->isRepeated()) {
// Make sure the value is an array of values
$v = is_array($v) ? $v : [$v];
$arr = [];
foreach ($v as $k => $vv) {
// Skip nullified repeated values
if (null === $vv) {
continue;
}
$filteredValue = $this->filterValue($vv, $field);
if ($this->isKeyValueMessage($vv)) {
$arr[key($filteredValue)] = current($filteredValue);
} else {
$arr[$k] = $filteredValue;
}
$v = $arr;
}
} else {
$v = $this->filterValue($v, $field);
}
$key = ($this->useCamelCase) ? $this->toCamelCase($key) : $key;
if (isset($this->customFilters[$key])) {
$v = call_user_func($this->customFilters[$key], $v);
}
$data[$key] = $v;
}
return $data;
}
/**
* Borrowed heavily from {@see DrSlump\Protobuf\Codec\PhpArray::decodeMessage()}.
* The only addition here is converting camel case field names to snake case.
*/
protected function decodeMessage(Protobuf\Message $message, $data)
{
// Get message descriptor
$descriptor = Protobuf::getRegistry()->getDescriptor($message);
foreach ($data as $key => $v) {
// Get the field by tag number or name
$field = $this->useTagNumber
? $descriptor->getField($key)
: $descriptor->getFieldByName($this->toSnakeCase($key));
// Unknown field found
if (!$field) {
$unknown = new Protobuf\Codec\PhpArray\Unknown($key, gettype($v), $v);
$message->addUnknown($unknown);
continue;
}
if ($field->isRepeated()) {
// Make sure the value is an array of values
$v = is_array($v) && is_int(key($v)) ? $v : [$v];
foreach ($v as $k => $vv) {
$v[$k] = $this->filterValue($vv, $field);
}
} else {
$v = $this->filterValue($v, $field);
}
$message->_set($field->getNumber(), $v);
}
return $message;
}
protected function filterValue($value, Protobuf\Field $field)
{
if (trim($field->getReference(), '\\') === NullValue::class) {
return null;
}
if ($value instanceof Protobuf\Message) {
if ($this->isKeyValueMessage($value)) {
$v = $value->getValue();
return [
$value->getKey() => $v instanceof Protobuf\Message
? $this->encodeMessage($v)
: $v
];
}
if ($value instanceof Struct) {
$vals = [];
foreach ($value->getFields() as $field) {
$val = $this->filterValue(
$field->getValue(),
$field->descriptor()->getFieldByName('value')
);
$vals[$field->getKey()] = $val;
}
return $vals;
}
if ($value instanceof ListValue) {
$vals = [];
foreach ($value->getValuesList() as $val) {
$fields = $val->descriptor()->getFields();
foreach ($fields as $field) {
$name = $field->getName();
if ($val->$name !== null) {
$vals[] = $this->filterValue($val->$name, $field);
}
}
}
return $vals;
}
if ($value instanceof Value) {
$fields = $value->descriptor()->getFields();
foreach ($fields as $field) {
$name = $field->getName();
if ($value->$name !== null) {
return $this->filterValue($value->$name, $field);
}
}
}
}
return parent::filterValue($value, $field);
}
private function toSnakeCase($key)
{
return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $key));
}
private function toCamelCase($key)
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
}
private function isKeyValueMessage($value)
{
return property_exists($value, 'key') && property_exists($value, 'value');
}
}

View File

@@ -0,0 +1,144 @@
<?php
/**
* Copyright 2024 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Report;
use Google\Cloud\Core\Compute\Metadata;
/**
* A MetadataProvider for Cloud Run Jobs.
*/
class CloudRunJobMetadataProvider implements MetadataProviderInterface
{
/**
* @var Metadata
*/
private $metadata;
/**
* @var string
*/
private $serviceId;
/**
* @var string
*/
private $revisionId;
/**
* @var string
*/
private $taskIndex;
/**
* @var string
*/
private $taskAttempt;
/**
* @var string
*/
private $instanceId;
/**
* @var string
*/
private $traceId;
/**
* @var string
*/
private $region;
/**
* @see https://cloud.google.com/run/docs/container-contract#jobs-env-vars
* @param array $env
*/
public function __construct(array $env, ?Metadata $metadata = null)
{
$this->serviceId = $env['CLOUD_RUN_JOB'] ?? 'unknown-job';
$this->revisionId = $env['CLOUD_RUN_EXECUTION'] ?? '';
$this->taskIndex = $env['CLOUD_RUN_TASK_INDEX'] ?? '';
$this->taskAttempt = $env['CLOUD_RUN_TASK_ATTEMPT'] ?? '';
$this->traceId = isset($env['HTTP_X_CLOUD_TRACE_CONTEXT'])
? \substr($env['HTTP_X_CLOUD_TRACE_CONTEXT'], 0, 32)
: null;
$this->metadata = $metadata ?? new Metadata();
$this->region = \basename($this->metadata->get('instance/region') ?? '');
$this->instanceId = $this->metadata->get('instance/id');
}
/**
* @return array
*/
public function monitoredResource()
{
return [
'type' => 'cloud_run_job',
'labels' => [
'job_name' => $this->serviceId,
'location' => $this->region,
'project_id' => $this->projectId(),
],
];
}
/**
* Return the project id.
* @return string
*/
public function projectId()
{
return $this->metadata->getProjectId();
}
/**
* Return the service id.
* @return string
*/
public function serviceId()
{
return $this->serviceId;
}
/**
* Return the version id.
* @return string
*/
public function versionId()
{
return $this->revisionId;
}
/**
* Return the labels.
* @return array
*/
public function labels()
{
$labels = [
'instanceId' => $this->instanceId,
'run.googleapis.com/execution_name' => $this->revisionId,
'run.googleapis.com/task_attempt' => $this->taskAttempt,
'run.googleapis.com/task_index' => $this->taskIndex,
'run.googleapis.com/trace_id' => $this->traceId,
];
return \array_filter($labels);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Copyright 2023 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Report;
/**
* @deprecated Use \Google\Cloud\Core\Report\CloudRunServiceMetadataProvider instead
*/
class CloudRunMetadataProvider extends CloudRunServiceMetadataProvider
{
}

View File

@@ -0,0 +1,138 @@
<?php
/**
* Copyright 2024 Google LLC
*
* 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.
*/
namespace Google\Cloud\Core\Report;
use Google\Cloud\Core\Compute\Metadata;
/**
* A MetadataProvider for Cloud Run Services.
*/
class CloudRunServiceMetadataProvider implements MetadataProviderInterface
{
/**
* @var Metadata
*/
private $metadata;
/**
* @var string
*/
private $configurationId;
/**
* @var string
*/
private $serviceId;
/**
* @var string
*/
private $revisionId;
/**
* @var string
*/
private $instanceId;
/**
* @var string
*/
private $traceId;
/**
* @var string
*/
private $region;
/**
* @see https://cloud.google.com/run/docs/container-contract#services-env-vars
* @param array $env
*/
public function __construct(array $env, ?Metadata $metadata = null)
{
$this->configurationId = $env['K_CONFIGURATION'] ?? 'unknown-configuration';
$this->serviceId = $env['K_SERVICE'] ?? 'unknown-service';
$this->revisionId = $env['K_REVISION'] ?? 'unknown-revision';
$this->traceId = isset($env['HTTP_X_CLOUD_TRACE_CONTEXT'])
? \substr($env['HTTP_X_CLOUD_TRACE_CONTEXT'], 0, 32)
: null;
$this->metadata = $metadata ?? new Metadata();
$this->region = \basename($this->metadata->get('instance/region') ?? '');
$this->instanceId = $this->metadata->get('instance/id');
}
/**
* @return array
*/
public function monitoredResource()
{
return [
'type' => 'cloud_run_revision',
'labels' => [
'configuration_name' => $this->configurationId,
'location' => $this->region,
'project_id' => $this->projectId(),
'revision_name' => $this->revisionId,
'service_name' => $this->serviceId,
],
];
}
/**
* Return the project id.
* @return string
*/
public function projectId()
{
return $this->metadata->getProjectId();
}
/**
* Return the service id.
* @return string
*/
public function serviceId()
{
return $this->serviceId;
}
/**
* Return the version id.
* @return string
*/
public function versionId()
{
return $this->revisionId;
}
/**
* Return the labels. We need to evaluate $_SERVER for each request.
*
* @return array
* @todo Figure out where to get the `container_name` from
*/
public function labels()
{
$labels = [
'instanceId' => $this->instanceId,
'run.googleapis.com/trace_id' => $this->traceId,
];
return \array_filter($labels);
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Report;
/**
* An empty MetadataProvider.
*/
class EmptyMetadataProvider implements MetadataProviderInterface
{
/**
* Return an array representing MonitoredResource.
*
* @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource
*
* @return array
*/
public function monitoredResource()
{
return [];
}
/**
* Return the project id.
* @return string
*/
public function projectId()
{
return '';
}
/**
* Return the service id.
* @return string
*/
public function serviceId()
{
return '';
}
/**
* Return the version id.
* @return string
*/
public function versionId()
{
return '';
}
/**
* Return the labels.
* @return array
*/
public function labels()
{
return [];
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Report;
/**
* An MetadataProvider for GAE Flex.
*/
class GAEFlexMetadataProvider extends GAEMetadataProvider
{
protected function getTraceValue($server)
{
return substr($server['HTTP_X_CLOUD_TRACE_CONTEXT'], 0, 32);
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* Copyright 2018 Google LLC.
*
* 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.
*/
namespace Google\Cloud\Core\Report;
/**
* MetadataProvider for GAE.
*/
abstract class GAEMetadataProvider implements MetadataProviderInterface
{
abstract protected function getTraceValue($server);
/** @var array */
private $data;
/**
* @param array $server An array for holding the values. Normally just use
* $_SERVER.
*/
public function __construct(array $server)
{
$projectId = $server['GOOGLE_CLOUD_PROJECT'] ?? 'unknown-projectid';
$serviceId = $server['GAE_SERVICE'] ?? 'unknown-service';
$versionId = $server['GAE_VERSION'] ?? 'unknown-version';
$labels = isset($server['HTTP_X_CLOUD_TRACE_CONTEXT'])
? ['appengine.googleapis.com/trace_id' =>
$this->getTraceValue($server)]
: [];
$this->data =
[
'resource' => [
'type' => 'gae_app',
'labels' => [
'project_id' => $projectId,
'version_id' => $versionId,
'module_id' => $serviceId
]
],
'projectId' => $projectId,
'serviceId' => $serviceId,
'versionId' => $versionId,
'labels' => $labels
];
}
/**
* Return an array representing MonitoredResource.
*
* @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource
*
* @return array
*/
public function monitoredResource()
{
return $this->data['resource'];
}
/**
* Return the project id.
* @return string
*/
public function projectId()
{
return $this->data['projectId'];
}
/**
* Return the service id.
* @return string
*/
public function serviceId()
{
return $this->data['serviceId'];
}
/**
* Return the version id.
* @return string
*/
public function versionId()
{
return $this->data['versionId'];
}
/**
* Return the labels. We need to evaluate $_SERVER for each request.
* @return array
*/
public function labels()
{
return $this->data['labels'];
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Copyright 2018 Google LLC.
*
* 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.
*/
namespace Google\Cloud\Core\Report;
/**
* An MetadataProvider for GAE Standard.
*/
class GAEStandardMetadataProvider extends GAEMetadataProvider
{
protected function getTraceValue($server)
{
$traceId = substr($server['HTTP_X_CLOUD_TRACE_CONTEXT'], 0, 32);
if (isset($server['GOOGLE_CLOUD_PROJECT'])) {
return sprintf(
'projects/%s/traces/%s',
$server['GOOGLE_CLOUD_PROJECT'],
$traceId
);
}
return $traceId;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Report;
/**
* An interface for provide some metadata for reports.
*/
interface MetadataProviderInterface
{
/**
* Return an array representing MonitoredResource.
*
* @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource
*
* @return array
*/
public function monitoredResource();
/**
* Return the project id.
* @return string
*/
public function projectId();
/**
* Return the service id.
* @return string
*/
public function serviceId();
/**
* Return the version id.
* @return string
*/
public function versionId();
/**
* Return the labels.
* @return array
*/
public function labels();
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Report;
use Google\Cloud\Core\Compute\Metadata;
/**
* Utility class for MetadataProvider.
*/
class MetadataProviderUtils
{
/**
* Automatically choose the most appropriate MetadataProvider and return it.
*
* @param array $server Normally pass the $_SERVER.
* @return MetadataProviderInterface
*/
public static function autoSelect($server, ?Metadata $metadata = null)
{
if (isset($server['GAE_SERVICE'])) {
if (isset($server['GAE_ENV']) && $server['GAE_ENV'] === 'standard') {
return new GAEStandardMetadataProvider($server);
}
return new GAEFlexMetadataProvider($server);
}
if (!empty(\getenv('K_CONFIGURATION'))) {
return new CloudRunServiceMetadataProvider(\getenv(), $metadata);
}
if (!empty(\getenv('CLOUD_RUN_JOB'))) {
return new CloudRunJobMetadataProvider(\getenv(), $metadata);
}
return new EmptyMetadataProvider();
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core\Report;
/**
* Simple MetadataProvider.
*/
class SimpleMetadataProvider implements MetadataProviderInterface
{
/** @var array */
private $data = [];
/**
* @param array $monitoredResource.
* {@see https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource}
* @param string $projectId [optional] **Defaults to** ''
* @param string $serviceId [optional] **Defaults to** ''
* @param string $versionId [optional] **Defaults to** ''
* @param array $labels [optional] **Defaults to** []
*/
public function __construct(
$monitoredResource = [],
$projectId = '',
$serviceId = '',
$versionId = '',
$labels = []
) {
$this->data['monitoredResource'] = $monitoredResource;
$this->data['projectId'] = $projectId;
$this->data['serviceId'] = $serviceId;
$this->data['versionId'] = $versionId;
$this->data['labels'] = $labels;
}
/**
* Return an array representing MonitoredResource.
*
* @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource
*
* @return array
*/
public function monitoredResource()
{
return $this->data['monitoredResource'];
}
/**
* Return the project id.
* @return string
*/
public function projectId()
{
return $this->data['projectId'];
}
/**
* Return the service id.
* @return string
*/
public function serviceId()
{
return $this->data['serviceId'];
}
/**
* Return the version id.
* @return string
*/
public function versionId()
{
return $this->data['versionId'];
}
/**
* Return the labels.
* @return array
*/
public function labels()
{
return $this->data['labels'];
}
}

View File

@@ -0,0 +1,172 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\RequestInterface;
/**
* Builds a PSR7 request from a service definition.
*/
class RequestBuilder
{
use JsonTrait;
use UriTrait;
/**
* @var string
*/
private $servicePath;
/**
* @var string
*/
private $baseUri;
/**
* @var array
*/
private $resourceRoot;
/**
* @var array
*/
private $service;
/**
* @param string $servicePath
* @param string $baseUri
* @param array $resourceRoot [optional]
*/
public function __construct($servicePath, $baseUri, array $resourceRoot = [])
{
$this->service = $this->loadServiceDefinition($servicePath);
$this->resourceRoot = $resourceRoot;
// Append service definition base path if bare apiEndpoint domain is given.
if (isset($this->service['basePath'])) {
$uriParts = parse_url($baseUri) + ['path' => null];
if (!$uriParts['path'] || $uriParts['path'] === '/') {
$uriParts['path'] = $this->service['basePath'];
// Recreate the URI from its modified parts and ensure it ends in a single slash.
$this->baseUri = rtrim((string) Uri::fromParts($uriParts), '/') . '/';
return;
}
}
$this->baseUri = rtrim($baseUri, '/') . '/';
}
/**
* Build the request.
*
* @param string $resource
* @param string $method
* @param array $options [optional]
* @return RequestInterface
* @todo complexity high, revisit
* @todo consider validating against the schemas
*/
public function build($resource, $method, array $options = [])
{
$root = $this->resourceRoot;
array_push($root, 'resources');
$root = array_merge($root, explode('.', $resource));
array_push($root, 'methods', $method);
$action = $this->service;
foreach ($root as $rootItem) {
if (!isset($action[$rootItem])) {
throw new \InvalidArgumentException('Provided path item ' . $rootItem . ' does not exist.');
}
$action = $action[$rootItem];
}
$path = [];
$query = [];
$body = [];
if (isset($action['parameters'])) {
foreach ($action['parameters'] as $parameter => $parameterOptions) {
if ($parameterOptions['location'] === 'path' && array_key_exists($parameter, $options)) {
$path[$parameter] = $options[$parameter];
unset($options[$parameter]);
}
if ($parameterOptions['location'] === 'query') {
// flatten nested querystring parameters into dot-syntax
if (false !== strpos($parameter, '.')) {
list($object, $property) = explode('.', $parameter, 2);
if (isset($options[$object][$property])) {
$options[$parameter] = $options[$object][$property];
}
}
if (array_key_exists($parameter, $options)) {
$query[$parameter] = $options[$parameter];
}
}
}
}
if (isset($this->service['parameters'])) {
foreach ($this->service['parameters'] as $parameter => $parameterOptions) {
if ($parameterOptions['location'] === 'query' && array_key_exists($parameter, $options)) {
$query[$parameter] = $options[$parameter];
}
}
}
if (isset($action['request'])) {
$schema = $action['request']['$ref'];
foreach ($this->service['schemas'][$schema]['properties'] as $property => $propertyOptions) {
if (array_key_exists($property, $options)) {
$body[$property] = $options[$property];
}
}
}
$uri = $this->buildUriWithQuery(
$this->expandUri($this->baseUri . $action['path'], $path),
$query
);
return new Request(
$action['httpMethod'],
$uri,
['Content-Type' => 'application/json'],
$body ? $this->jsonEncode($body) : null
);
}
/**
* @param string $servicePath
* @return array
*/
private function loadServiceDefinition($servicePath)
{
return $this->jsonDecode(
file_get_contents($servicePath, true),
true
);
}
}

View File

@@ -0,0 +1,142 @@
<?php
/**
* Copyright 2023 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use \Google\Protobuf\Internal\Message;
use Google\ApiCore\ApiException;
use Google\ApiCore\OperationResponse;
use Google\ApiCore\Serializer;
use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Core\Exception\ServiceException;
/**
* @internal
* Responsible for forwarding the requests to their
* respective client methdos via the request wrapper.
*/
class RequestHandler
{
use EmulatorTrait;
use ArrayTrait;
use TimeTrait;
use WhitelistTrait;
use RequestProcessorTrait;
/**
* @var Serializer
*/
private Serializer $serializer;
private array $clients = [];
/**
* @param Serializer $serializer
* @param array<string|object> $clients
* @param array $clientConfig
*/
public function __construct(
Serializer $serializer,
array $clients,
array $clientConfig = []
) {
//@codeCoverageIgnoreStart
$this->serializer = $serializer;
$clientConfig['serializer'] = $serializer;
// Adds some defaults
// gccl needs to be present for handwritten clients
$clientConfig += [
'libName' => 'gccl',
'emulatorHost' => null
];
if ((bool) $clientConfig['emulatorHost']) {
$emulatorConfig = $this->emulatorGapicConfig($clientConfig['emulatorHost']);
$clientConfig = array_merge(
$clientConfig,
$emulatorConfig
);
}
//@codeCoverageIgnoreEnd
// Initialize the client classes and store them in memory
foreach ($clients as $client) {
if (is_object($client)) {
$this->clients[get_class($client)] = $client;
} else {
$this->clients[$client] = new $client($clientConfig);
}
}
}
/**
* Helper function that forwards the request to a client obj.
*
* @param string $clientClass The request will be forwarded to this client class.
* @param string $method This method needs to be called on the client obj.
* @param Message $request The protobuf Request instance to pass as the first argument to the $method.
* @param array $optionalArgs The optional args.
* @param bool $whitelisted This decides the behaviour when a NotFoundException is encountered.
*
* @return \Generator|OperationResponse|array|null
*
* @throws ServiceException
*/
public function sendRequest(
string $clientClass,
string $method,
Message $request,
array $optionalArgs,
bool $whitelisted = false
) {
$clientObj = $this->getClientObject($clientClass);
if (!$clientObj) {
return null;
}
$allArgs = [$request];
$allArgs[] = $optionalArgs;
try {
$callable = [$clientObj, $method];
$response = call_user_func_array($callable, $allArgs);
return $this->handleResponse($response);
} catch (ApiException $ex) {
throw $this->convertToGoogleException($ex);
} catch (NotFoundException $e) {
if ($whitelisted) {
throw $this->modifyWhitelistedError($e);
}
throw $e;
}
}
/**
* Helper function that returns a client object stored in memory
* using the client class as key.
* @param $clientClass The client class whose object we need.
* @return mixed
*/
private function getClientObject(string $clientClass)
{
return $this->clients[$clientClass] ?? null;
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use \Google\Protobuf\Internal\Message;
use Google\ApiCore\OperationResponse;
use Google\ApiCore\PagedListResponse;
use Google\ApiCore\ServerStream;
use Google\Cloud\Core\Exception\ServiceException;
use Google\Rpc\BadRequest;
use Google\Rpc\Code;
use Google\Rpc\RetryInfo;
/**
* @internal
* Encapsulates shared functionality of classes that need to send
* and process requests and responses.
*/
trait RequestProcessorTrait
{
/**
* @var array Map of error metadata types to RPC wrappers.
*/
private $metadataTypes = [
'google.rpc.retryinfo-bin' => RetryInfo::class,
'google.rpc.badrequest-bin' => BadRequest::class
];
/**
* Serializes a gRPC response.
*
* @param mixed $response
* @return \Generator|OperationResponse|array|null
*/
private function handleResponse($response)
{
if ($response instanceof PagedListResponse) {
$response = $response->getPage()->getResponseObject();
}
if ($response instanceof Message) {
return $this->serializer->encodeMessage($response);
}
if ($response instanceof OperationResponse) {
return $response;
}
if ($response instanceof ServerStream) {
return $this->handleStream($response);
}
return null;
}
/**
* Handles a streaming response.
*
* @param ServerStream $response
* @return \Generator|array|null
* @throws Exception\ServiceException
*/
private function handleStream(ServerStream $response)
{
try {
foreach ($response->readAll() as $count => $result) {
$res = $this->serializer->encodeMessage($result);
yield $res;
}
} catch (\Exception $ex) {
throw $this->convertToGoogleException($ex);
}
}
/**
* Convert a ApiCore exception to a Google Exception.
*
* @param \Exception $ex
* @return ServiceException
*/
private function convertToGoogleException(\Exception $ex): ServiceException
{
switch ($ex->getCode()) {
case Code::INVALID_ARGUMENT:
$exception = Exception\BadRequestException::class;
break;
case Code::NOT_FOUND:
case Code::UNIMPLEMENTED:
$exception = Exception\NotFoundException::class;
break;
case Code::ALREADY_EXISTS:
$exception = Exception\ConflictException::class;
break;
case Code::FAILED_PRECONDITION:
$exception = Exception\FailedPreconditionException::class;
break;
case Code::UNKNOWN:
$exception = Exception\ServerException::class;
break;
case Code::INTERNAL:
$exception = Exception\ServerException::class;
break;
case Code::ABORTED:
$exception = Exception\AbortedException::class;
break;
case Code::DEADLINE_EXCEEDED:
$exception = Exception\DeadlineExceededException::class;
break;
default:
$exception = Exception\ServiceException::class;
break;
}
$metadata = [];
if (method_exists($ex, 'getMetadata') && $ex->getMetadata()) {
foreach ($ex->getMetadata() as $type => $binaryValue) {
if (!isset($this->metadataTypes[$type])) {
continue;
}
$metadataElement = new $this->metadataTypes[$type]();
$metadataElement->mergeFromString($binaryValue[0]);
$metadata[] = $this->serializer->encodeMessage($metadataElement);
}
}
return new $exception($ex->getMessage(), $ex->getCode(), $ex, $metadata);
}
}

View File

@@ -0,0 +1,541 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\Auth\FetchAuthTokenCache;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\GetUniverseDomainInterface;
use Google\Auth\HttpHandler\Guzzle6HttpHandler;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\UpdateMetadataInterface;
use Google\Cloud\Core\Exception\GoogleException;
use Google\Cloud\Core\Exception\ServiceException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* The RequestWrapper is responsible for delivering and signing requests.
*/
class RequestWrapper
{
use RequestWrapperTrait;
use RetryDeciderTrait;
/**
* @var string|null The current version of the component from which the request
* originated.
*/
private $componentVersion;
/**
* @var string|null Access token used to sign requests.
*/
private $accessToken;
/**
* @var callable A handler used to deliver PSR-7 requests specifically for
* authentication.
*/
private $authHttpHandler;
/**
* @var callable A handler used to deliver PSR-7 requests.
*/
private $httpHandler;
/**
* @var callable A handler used to deliver PSR-7 requests asynchronously.
*/
private $asyncHttpHandler;
/**
* @var array HTTP client specific configuration options.
*/
private $restOptions;
/**
* @var bool Whether to enable request signing.
*/
private $shouldSignRequest;
/**
* @var callable Sets the conditions for whether or not a
* request should attempt to retry.
*/
private $retryFunction;
/**
* @var callable Executes a delay.
*/
private $delayFunction;
/**
* @var callable|null Sets the conditions for determining how long to wait
* between attempts to retry.
*/
private $calcDelayFunction;
/**
* @var string The universe domain to verify against the credentials.
*/
private string $universeDomain;
/**
* @var bool Ensure we only check the universe domain once.
*/
private bool $hasCheckedUniverse = false;
/**
* @param array $config [optional] {
* Configuration options. Please see
* {@see \Google\Cloud\Core\RequestWrapperTrait::setCommonDefaults()} for
* the other available options.
*
* @type string $componentVersion The current version of the component from
* which the request originated.
* @type string $accessToken Access token used to sign requests.
* Deprecated: This option is no longer supported. Use the `$credentialsFetcher` option instead.
* @type callable $asyncHttpHandler *Experimental* A handler used to
* deliver PSR-7 requests asynchronously. Function signature should match:
* `function (RequestInterface $request, array $options = []) : PromiseInterface<ResponseInterface>`.
* @type callable $authHttpHandler A handler used to deliver PSR-7
* requests specifically for authentication. Function signature
* should match:
* `function (RequestInterface $request, array $options = []) : ResponseInterface`.
* @type callable $httpHandler A handler used to deliver PSR-7 requests.
* Function signature should match:
* `function (RequestInterface $request, array $options = []) : ResponseInterface`.
* @type array $restOptions HTTP client specific configuration options.
* @type bool $shouldSignRequest Whether to enable request signing.
* @type callable $restRetryFunction Sets the conditions for whether or
* not a request should attempt to retry. Function signature should
* match: `function (\Exception $ex) : bool`.
* @type callable $restDelayFunction Executes a delay, defaults to
* utilizing `usleep`. Function signature should match:
* `function (int $delay) : void`.
* @type callable $restCalcDelayFunction Sets the conditions for
* determining how long to wait between attempts to retry. Function
* signature should match: `function (int $attempt) : int`.
* @type string $universeDomain The expected universe of the credentials. Defaults to "googleapis.com".
* }
*/
public function __construct(array $config = [])
{
$this->setCommonDefaults($config);
$config += [
'accessToken' => null,
'asyncHttpHandler' => null,
'authHttpHandler' => null,
'httpHandler' => null,
'restOptions' => [],
'shouldSignRequest' => true,
'componentVersion' => null,
'restRetryFunction' => null,
'restDelayFunction' => null,
'restCalcDelayFunction' => null,
'universeDomain' => GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN,
];
$this->componentVersion = $config['componentVersion'];
$this->accessToken = $config['accessToken'];
$this->restOptions = $config['restOptions'];
$this->shouldSignRequest = $config['shouldSignRequest'];
$this->retryFunction = $config['restRetryFunction'] ?: $this->getRetryFunction();
$this->delayFunction = $config['restDelayFunction'] ?: function ($delay) {
usleep($delay);
};
$this->calcDelayFunction = $config['restCalcDelayFunction'];
$this->httpHandler = $config['httpHandler'] ?: HttpHandlerFactory::build();
$this->authHttpHandler = $config['authHttpHandler'] ?: $this->httpHandler;
$this->asyncHttpHandler = $config['asyncHttpHandler'] ?: $this->buildDefaultAsyncHandler();
$this->universeDomain = $config['universeDomain'];
if ($this->credentialsFetcher instanceof AnonymousCredentials) {
$this->shouldSignRequest = false;
}
}
/**
* Deliver the request.
*
* @param RequestInterface $request A PSR-7 request.
* @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 callable $restRetryFunction Sets the conditions for whether or
* not a request should attempt to retry. Function signature should
* match: `function (\Exception $ex) : bool`.
* @type callable $restRetryListener Runs after the restRetryFunction.
* This might be used to simply consume the exception and
* $arguments b/w retries. This returns the new $arguments thus
* allowing modification on demand for $arguments. For ex:
* changing the headers in b/w retries.
* @type callable $restDelayFunction Executes a delay, defaults to
* utilizing `usleep`. Function signature should match:
* `function (int $delay) : void`.
* @type callable $restCalcDelayFunction Sets the conditions for
* determining how long to wait between attempts to retry. Function
* signature should match: `function (int $attempt) : int`.
* @type array $restOptions HTTP client specific configuration options.
* }
* @return ResponseInterface
* @throws ServiceException
*/
public function send(RequestInterface $request, array $options = [])
{
$retryOptions = $this->getRetryOptions($options);
$backoff = new ExponentialBackoff(
$retryOptions['retries'],
$retryOptions['retryFunction'],
$retryOptions['retryListener'],
);
if ($retryOptions['delayFunction']) {
$backoff->setDelayFunction($retryOptions['delayFunction']);
}
if ($retryOptions['calcDelayFunction']) {
$backoff->setCalcDelayFunction($retryOptions['calcDelayFunction']);
}
try {
return $backoff->execute($this->httpHandler, [
$this->applyHeaders($request, $options),
$this->getRequestOptions($options)
]);
} catch (\Exception $ex) {
throw $this->convertToGoogleException($ex);
}
}
/**
* Deliver the request asynchronously.
*
* @param RequestInterface $request A PSR-7 request.
* @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 callable $restRetryFunction Sets the conditions for whether or
* not a request should attempt to retry. Function signature should
* match: `function (\Exception $ex, int $retryAttempt) : bool`.
* @type callable $restDelayFunction Executes a delay, defaults to
* utilizing `usleep`. Function signature should match:
* `function (int $delay) : void`.
* @type callable $restCalcDelayFunction Sets the conditions for
* determining how long to wait between attempts to retry. Function
* signature should match: `function (int $attempt) : int`.
* @type array $restOptions HTTP client specific configuration options.
* }
* @return PromiseInterface<ResponseInterface>
* @throws ServiceException
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
public function sendAsync(RequestInterface $request, array $options = [])
{
// Unfortunately, the current ExponentialBackoff implementation doesn't
// play nicely with promises.
$retryAttempt = 0;
$fn = function ($retryAttempt) use (&$fn, $request, $options) {
$asyncHttpHandler = $this->asyncHttpHandler;
$retryOptions = $this->getRetryOptions($options);
if (!$retryOptions['calcDelayFunction']) {
$retryOptions['calcDelayFunction'] = [ExponentialBackoff::class, 'calculateDelay'];
}
return $asyncHttpHandler(
$this->applyHeaders($request, $options),
$this->getRequestOptions($options)
)->then(null, function (\Exception $ex) use ($fn, $retryAttempt, $retryOptions) {
$shouldRetry = $retryOptions['retryFunction']($ex, $retryAttempt);
if ($shouldRetry === false || $retryAttempt >= $retryOptions['retries']) {
throw $this->convertToGoogleException($ex);
}
$delay = $retryOptions['calcDelayFunction']($retryAttempt);
$retryOptions['delayFunction']($delay);
$retryAttempt++;
return $fn($retryAttempt);
});
};
return $fn($retryAttempt);
}
/**
* Applies headers to the request.
*
* @param RequestInterface $request A PSR-7 request.
* @param array $options
* @return RequestInterface
*/
private function applyHeaders(RequestInterface $request, array $options = [])
{
$headers = [
'User-Agent' => 'gcloud-php/' . $this->componentVersion,
Retry::RETRY_HEADER_KEY => sprintf(
'gl-php/%s gccl/%s',
PHP_VERSION,
$this->componentVersion
),
];
if (isset($options['retryHeaders'])) {
$headers[Retry::RETRY_HEADER_KEY] = sprintf(
'%s %s',
$headers[Retry::RETRY_HEADER_KEY],
implode(' ', $options['retryHeaders'])
);
unset($options['retryHeaders']);
}
$request = Utils::modifyRequest($request, ['set_headers' => $headers]);
if ($this->shouldSignRequest) {
$quotaProject = $this->quotaProject;
if ($this->accessToken) {
// if an access token is provided, check the universe domain against "googleapis.com"
$this->checkUniverseDomain(null);
$request = $request->withHeader('authorization', 'Bearer ' . $this->accessToken);
} else {
// if a credentials fetcher is provided, check the universe domain against the
// credential's universe domain
$credentialsFetcher = $this->getCredentialsFetcher();
$this->checkUniverseDomain($credentialsFetcher);
$request = $this->addAuthHeaders($request, $credentialsFetcher);
if ($credentialsFetcher instanceof GetQuotaProjectInterface) {
$quotaProject = $credentialsFetcher->getQuotaProject();
}
}
if ($quotaProject) {
$request = $request->withHeader('X-Goog-User-Project', $quotaProject);
}
} else {
// If we are not signing the request, check the universe domain against "googleapis.com"
$this->checkUniverseDomain(null);
}
return $request;
}
/**
* Adds auth headers to the request.
*
* @param RequestInterface $request
* @param FetchAuthTokenInterface $fetcher
* @return array
* @throws ServiceException
*/
private function addAuthHeaders(RequestInterface $request, FetchAuthTokenInterface $fetcher)
{
$backoff = new ExponentialBackoff($this->retries, $this->getRetryFunction());
try {
return $backoff->execute(
function () use ($request, $fetcher) {
if (!$fetcher instanceof UpdateMetadataInterface ||
(
$fetcher instanceof FetchAuthTokenCache &&
!$fetcher->getFetcher() instanceof UpdateMetadataInterface
)
) {
// This covers an edge case where the token fetcher does not implement UpdateMetadataInterface,
// which only would happen if a user implemented a custom fetcher
if ($token = $fetcher->fetchAuthToken($this->authHttpHandler)) {
return $request->withHeader('authorization', 'Bearer ' . $token['access_token']);
}
} else {
$headers = $fetcher->updateMetadata($request->getHeaders(), null, $this->authHttpHandler);
return Utils::modifyRequest($request, ['set_headers' => $headers]);
}
// As we do not know the reason the credentials fetcher could not fetch the
// token, we should not retry.
throw new \RuntimeException('Unable to fetch token');
}
);
} catch (\Exception $ex) {
throw $this->convertToGoogleException($ex);
}
}
/**
* Convert any exception to a Google Exception.
*
* @param \Exception $ex
* @return Exception\ServiceException
*/
private function convertToGoogleException(\Exception $ex)
{
switch ($ex->getCode()) {
case 400:
$exception = Exception\BadRequestException::class;
break;
case 404:
$exception = Exception\NotFoundException::class;
break;
case 409:
$exception = Exception\ConflictException::class;
break;
case 412:
$exception = Exception\FailedPreconditionException::class;
break;
case 500:
$exception = Exception\ServerException::class;
break;
case 504:
$exception = Exception\DeadlineExceededException::class;
break;
default:
$exception = Exception\ServiceException::class;
break;
}
return new $exception($this->getExceptionMessage($ex), $ex->getCode(), $ex);
}
/**
* Gets the exception message.
*
* @access private
* @param \Exception $ex
* @return string
*/
private function getExceptionMessage(\Exception $ex)
{
if ($ex instanceof RequestException && $ex->hasResponse()) {
return (string) $ex->getResponse()->getBody();
}
return $ex->getMessage();
}
/**
* Gets a set of request options.
*
* @param array $options
* @return array
*/
private function getRequestOptions(array $options)
{
$restOptions = $options['restOptions'] ?? $this->restOptions;
$timeout = $options['requestTimeout'] ?? $this->requestTimeout;
if ($timeout && !array_key_exists('timeout', $restOptions)) {
$restOptions['timeout'] = $timeout;
}
return $restOptions;
}
/**
* Gets a set of retry options.
*
* @param array $options
* @return array
*/
private function getRetryOptions(array $options)
{
return [
'retries' => isset($options['retries'])
? $options['retries']
: $this->retries,
'retryFunction' => isset($options['restRetryFunction'])
? $options['restRetryFunction']
: $this->retryFunction,
'retryListener' => isset($options['restRetryListener'])
? $options['restRetryListener']
: null,
'delayFunction' => isset($options['restDelayFunction'])
? $options['restDelayFunction']
: $this->delayFunction,
'calcDelayFunction' => isset($options['restCalcDelayFunction'])
? $options['restCalcDelayFunction']
: $this->calcDelayFunction
];
}
/**
* Builds the default async HTTP handler.
*
* @return callable
*/
private function buildDefaultAsyncHandler()
{
return $this->httpHandler instanceof Guzzle6HttpHandler
? [$this->httpHandler, 'async']
: [HttpHandlerFactory::build(), 'async'];
}
/**
* Verify that the expected universe domain matches the universe domain from the credentials.
*/
private function checkUniverseDomain(?FetchAuthTokenInterface $credentialsFetcher = null)
{
if (false === $this->hasCheckedUniverse) {
if ($this->universeDomain === '') {
throw new GoogleException('The universe domain cannot be empty.');
}
if (is_null($credentialsFetcher)) {
if ($this->universeDomain !== GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN) {
throw new GoogleException(sprintf(
'The accessToken option is not supported outside of the default universe domain (%s).',
GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN
));
}
} else {
$credentialsUniverse = $credentialsFetcher instanceof GetUniverseDomainInterface
? $credentialsFetcher->getUniverseDomain()
: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
if ($credentialsUniverse !== $this->universeDomain) {
throw new GoogleException(sprintf(
'The configured universe domain (%s) does not match the credential universe domain (%s)',
$this->universeDomain,
$credentialsUniverse
));
}
}
$this->hasCheckedUniverse = true;
}
}
}

View File

@@ -0,0 +1,220 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\Auth\ApplicationDefaultCredentials;
use Google\Auth\Cache\MemoryCacheItemPool;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\FetchAuthTokenCache;
use Google\Auth\FetchAuthTokenInterface;
use Psr\Cache\CacheItemPoolInterface;
/**
* Encapsulates shared functionality of request wrappers.
*/
trait RequestWrapperTrait
{
/**
* @var CacheItemPoolInterface A cache used for storing tokens.
*/
private $authCache;
/**
* @var array Cache configuration options.
*/
private $authCacheOptions;
/**
* @var FetchAuthTokenInterface|null Fetches credentials.
*/
private $credentialsFetcher;
/**
* @var array The contents of the service account credentials .json file
* retrieved from the Google Developers Console.
*/
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`.
*/
private $retries;
/**
* @var array Scopes to be used for the request.
*/
private $scopes = [];
/**
* @var string|null The user project to bill for access charges associated
* with the request.
*/
private $quotaProject;
/**
* Sets common defaults between request wrappers.
*
* @param array $config {
* Configuration options.
*
* @type CacheItemPoolInterface $authCache A cache for storing access
* tokens. **Defaults to** a simple in memory implementation.
* @type array $authCacheOptions Cache configuration options.
* @type FetchAuthTokenInterface $credentialsFetcher A credentials
* fetcher instance.
* @type array $keyFile [DEPRECATED] use `credentialsFetcher` option.
* @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.
* @type string $quotaProject Specifies a user project to bill for
* access charges associated with the request.
* }
* @throws \InvalidArgumentException
*/
public function setCommonDefaults(array $config)
{
$config += [
'authCache' => new MemoryCacheItemPool(),
'authCacheOptions' => [],
'credentialsFetcher' => null,
'keyFile' => null,
'requestTimeout' => null,
'retries' => 3,
'scopes' => null,
'quotaProject' => null
];
if ($config['credentialsFetcher'] && !$config['credentialsFetcher'] instanceof FetchAuthTokenInterface) {
throw new \InvalidArgumentException('credentialsFetcher must implement FetchAuthTokenInterface.');
}
if (!$config['authCache'] instanceof CacheItemPoolInterface) {
throw new \InvalidArgumentException('authCache must implement CacheItemPoolInterface.');
}
$this->authCache = $config['authCache'];
$this->authCacheOptions = $config['authCacheOptions'];
$this->credentialsFetcher = $config['credentialsFetcher'];
$this->retries = $config['retries'];
$this->scopes = $config['scopes'];
$this->keyFile = $config['keyFile'];
$this->requestTimeout = $config['requestTimeout'];
$this->quotaProject = $config['quotaProject'];
}
/**
* Get the Keyfile.
*
* @return array
*/
public function keyFile()
{
return $this->keyFile;
}
/**
* Get the scopes
*
* @return array
*/
public function scopes()
{
return $this->scopes;
}
/**
* Gets the credentials fetcher and sets up caching. Precedence is as
* follows:
*
* - A user supplied credentials fetcher instance.
* - Credentials created from a keyfile.
* - Application default credentials.
* - Anonymous credentials.
*
* @return FetchAuthTokenInterface
*/
public function getCredentialsFetcher()
{
$fetcher = null;
if ($this->credentialsFetcher) {
$fetcher = $this->credentialsFetcher;
} else {
if ($this->keyFile) {
if ($this->quotaProject) {
$this->keyFile['quota_project_id'] = $this->quotaProject;
}
$fetcher = CredentialsLoader::makeCredentials($this->scopes, $this->keyFile);
} else {
try {
$fetcher = $this->getADC();
} catch (\DomainException $ex) {
$fetcher = new AnonymousCredentials();
}
}
// Note: If authCache is set and keyFile is not set, the resulting
// credentials instance will be FetchAuthTokenCache, and we will be
// unable to enable "useJwtAccessWithScope". This is unlikely, as
// keyFile is automatically set in ClientTrait::configureAuthentication,
// and so should always exist when ServiceAccountCredentials are in use.
if ($fetcher instanceof ServiceAccountCredentials) {
$fetcher->useJwtAccessWithScope();
}
}
if ($fetcher instanceof FetchAuthTokenCache) {
// The fetcher has already been wrapped in a cache by `ApplicationDefaultCredentials`;
// no need to wrap it another time.
return $fetcher;
} else {
return new FetchAuthTokenCache(
$fetcher,
$this->authCacheOptions,
$this->authCache
);
}
}
/**
* Returns application default credentials. Abstracted out for unit testing.
*
* @return FetchAuthTokenInterface
* @throws \DomainException
*/
protected function getADC()
{
return ApplicationDefaultCredentials::getCredentials(
$this->scopes,
$this->authHttpHandler,
$this->authCacheOptions,
$this->authCache,
$this->quotaProject
);
}
}

View File

@@ -0,0 +1,165 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Core\Exception\ServiceException;
use UnexpectedValueException;
/**
* Provides shared functionality for REST service implementations.
*/
trait RestTrait
{
use ArrayTrait;
use JsonTrait;
use WhitelistTrait;
/**
* @var RequestBuilder Builds PSR7 requests from a service definition.
*/
private $requestBuilder;
/**
* @var RequestWrapper Wrapper used to handle sending requests to the
* JSON API.
*/
private $requestWrapper;
/**
* Sets the request builder.
*
* @param RequestBuilder $requestBuilder Builds PSR7 requests from a service
* definition.
*/
public function setRequestBuilder(RequestBuilder $requestBuilder)
{
$this->requestBuilder = $requestBuilder;
}
/**
* Sets the request wrapper.
*
* @param RequestWrapper $requestWrapper Wrapper used to handle sending
* requests to the JSON API.
*/
public function setRequestWrapper(RequestWrapper $requestWrapper)
{
$this->requestWrapper = $requestWrapper;
}
/**
* Get the RequestWrapper.
*
* @return RequestWrapper|null
*/
public function requestWrapper()
{
return $this->requestWrapper;
}
/**
* Delivers a request built from the service definition.
*
* @param string $resource The resource type used for the request.
* @param string $method The method used for the request.
* @param array $options [optional] Options used to build out the request.
* @param array $whitelisted [optional]
* @return array
* @throws ServiceException
*/
public function send($resource, $method, array $options = [], $whitelisted = false)
{
$options += [
'prettyPrint' => false,
];
$requestOptions = $this->pluckArray([
'restOptions',
'retries',
'retryHeaders',
'requestTimeout',
'restRetryFunction',
'restRetryListener',
'restDelayFunction',
'restCalcDelayFunction',
], $options);
try {
return json_decode(
$this->requestWrapper->send(
$this->requestBuilder->build($resource, $method, $options),
$requestOptions
)->getBody(),
true
);
} catch (NotFoundException $e) {
if ($whitelisted) {
throw $this->modifyWhitelistedError($e);
}
throw $e;
}
}
/**
* Return a custom API endpoint in the proper format, or default if none provided.
*
* @param string $default
* @param array $config
* @param string $apiEndpointTemplate
* @return string
*/
private function getApiEndpoint($default, array $config, ?string $apiEndpointTemplate = null)
{
// If the $default parameter is provided, or the user has set an "apiEndoint" config option,
// fall back to the previous behavior.
if ($res = $config['apiEndpoint'] ?? $default) {
if (substr($res, -1) !== '/') {
$res = $res . '/';
}
if (strpos($res, '//') === false) {
$res = 'https://' . $res;
}
return $res;
}
// One of the $default or the $template must always be set
if (!$apiEndpointTemplate) {
throw new UnexpectedValueException(
'An API endpoint template must be provided if no "apiEndpoint" or default endpoint is set.'
);
}
if (!isset($config['universeDomain'])) {
throw new UnexpectedValueException(
'The "universeDomain" config value must be set to use the default API endpoint template.'
);
}
$apiEndpoint = str_replace(
'UNIVERSE_DOMAIN',
$config['universeDomain'],
$apiEndpointTemplate
);
// Preserve the behavior of guaranteeing a trailing "/"
return $apiEndpoint . (substr($apiEndpoint, -1) !== '/' ? '/' : '');
}
}

116
vendor/google/cloud-core/src/Retry.php vendored Normal file
View File

@@ -0,0 +1,116 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
/**
* Retry implementation.
*
* Unlike {@see \Google\Cloud\Core\ExponentialBackoff}, Retry requires an implementor
* to supply wait times for each iteration.
*/
class Retry
{
const RETRY_HEADER_KEY = 'x-goog-api-client';
/**
* @var int
*/
private $retries;
/**
* @var callable
*/
private $delayFunction;
/**
* @var callable|null
*/
private $retryFunction;
/**
* @param int|null $retries Maximum number of retries for a failed request.
* @param callable $delayFunction A function returning an array of format
* `['seconds' => (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->delayFunction = $delayFunction;
$this->retryFunction = $retryFunction;
}
/**
* 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;
$continue = true;
do {
try {
$res = call_user_func_array($function, $arguments);
$continue = false;
return $res;
} catch (\Exception $exception) {
if ($this->retryFunction) {
if (!call_user_func($this->retryFunction, $exception, $retryAttempt)) {
throw $exception;
}
}
if ($retryAttempt < $this->retries) {
$delay = $delayFunction($exception);
$delay += [
'seconds' => 0,
'nanos' => 0
];
time_nanosleep($delay['seconds'], $delay['nanos']);
$retryAttempt++;
} else {
$continue = false;
throw $exception;
}
}
} while ($continue);
}
/**
* @param callable $delayFunction
* @return void
*/
public function setDelayFunction(callable $delayFunction)
{
$this->delayFunction = $delayFunction;
}
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* Copyright 2017 Google Inc.
*
* 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.
*/
namespace Google\Cloud\Core;
use GuzzleHttp\Exception\RequestException;
/**
* Provides methods for deciding if a request should be retried.
*/
trait RetryDeciderTrait
{
use JsonTrait;
/**
* @var array
*/
private $httpRetryCodes = [
500,
502,
503
];
/**
* @var array
*/
private $httpRetryMessages = [
'rateLimitExceeded',
'userRateLimitExceeded'
];
/**
* Determines whether or not the request should be retried.
*
* @param bool $shouldRetryMessages Whether or not to attempt retrying based
* on the failure message.
* @return callable
*/
private function getRetryFunction($shouldRetryMessages = true)
{
$httpRetryCodes = $this->httpRetryCodes;
$httpRetryMessages = $this->httpRetryMessages;
return function (\Exception $ex) use ($httpRetryCodes, $httpRetryMessages, $shouldRetryMessages) {
$statusCode = $ex->getCode();
if (in_array($statusCode, $httpRetryCodes)) {
return true;
}
if (!$shouldRetryMessages) {
return false;
}
$message = ($ex instanceof RequestException && $ex->hasResponse())
? (string) $ex->getResponse()->getBody()
: $ex->getMessage();
try {
$message = $this->jsonDecode(
$message,
true
);
} catch (\InvalidArgumentException $ex) {
return false;
}
if (!isset($message['error']['errors'])) {
return false;
}
foreach ($message['error']['errors'] as $error) {
if (in_array($error['reason'], $httpRetryMessages)) {
return true;
}
}
return false;
};
}
/**
* @param array $codes
*/
private function setHttpRetryCodes(array $codes)
{
$this->httpRetryCodes = $codes;
}
/**
* @param array $messages
*/
private function setHttpRetryMessages(array $messages)
{
$this->httpRetryMessages = $messages;
}
}

View File

@@ -0,0 +1,402 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
namespace Google\Cloud\Core;
use Google\Auth\HttpHandler\Guzzle6HttpHandler;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\Datastore\DatastoreClient;
use Google\Cloud\Firestore\FirestoreClient;
use Google\Cloud\Language\LanguageClient as DeprecatedLanguageClient;
use Google\Cloud\Language\V2\Client\LanguageServiceClient;
use Google\Cloud\Logging\LoggingClient;
use Google\Cloud\PubSub\PubSubClient;
use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Speech\SpeechClient as DeprecatedSpeechClient;
use Google\Cloud\Speech\V2\Client\SpeechClient;
use Google\Cloud\Storage\StorageClient;
use Google\Cloud\Trace\TraceClient;
use Google\Cloud\Translate\V2\TranslateClient as DeprecatedTranslateClient;
use Google\Cloud\Translate\V3\Client\TranslationServiceClient;
use Google\Cloud\Vision\V1\Client\ImageAnnotatorClient;
use Google\Cloud\Vision\VisionClient as DeprecatedVisionClient;
use Psr\Cache\CacheItemPoolInterface;
/**
* Google Cloud Platform is a set of modular cloud-based services that allow you
* to create anything from simple websites to complex applications.
*
* This API aims to expose access to these services in a way that is intuitive
* and easy to use for PHP enthusiasts. The ServiceBuilder instance exposes
* factory methods which grant access to the various services contained within
* the API.
*
* Configuration is simple. Pass in an array of configuration options to the
* constructor up front which can be shared between clients or specify the
* options for the specific services you wish to access, e.g. Datastore, or
* Storage.
*
* Please note that unless otherwise noted the examples below take advantage of
* [Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials).
*
* @deprecated
*/
class ServiceBuilder
{
/**
* @var array Configuration options to be used between clients.
*/
private $config = [];
/**
* Pass in an array of configuration options which will be shared between
* clients.
*
* Example:
* ```
* use Google\Cloud\Core\ServiceBuilder;
*
* $cloud = new ServiceBuilder([
* 'projectId' => 'myAwesomeProject'
* ]);
* ```
*
* @param array $config [optional] {
* Configuration options.
*
* @type string $projectId The project ID from the Google Developer's
* Console.
* @type CacheItemPoolInterface $authCache A cache for storing access
* tokens. **Defaults to** a simple in memory implementation.
* @type array $authCacheOptions Cache configuration options.
* @type callable $authHttpHandler A handler used to deliver Psr7
* requests specifically for authentication.
* @type callable $httpHandler A handler used to deliver Psr7 requests.
* Only valid for requests sent over REST.
* @type array $keyFile [DEPRECATED]
* @type string $keyFilePath [DEPRECATED]
* @type int $retries Number of retries for a failed request.
* **Defaults to** `3`.
* @type array $scopes Scopes to be used for the request.
* }
*/
public function __construct(array $config = [])
{
$this->config = $this->resolveConfig($config);
}
/**
* 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/docs).
*
* Example:
* ```
* $bigQuery = $cloud->bigQuery();
* ```
*
* @param array $config [optional] {
* Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the other available options.
*
* @type bool $returnInt64AsObject If true, 64 bit integers will be
* returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
* @type string $location If provided, determines the default geographic
* location used when creating datasets and managing jobs. Please
* note: This is only required for jobs started outside of the US
* and EU regions. Also, if location metadata has already been
* fetched over the network it will take precedent over this
* setting.
* }
* @return BigQueryClient
*/
public function bigQuery(array $config = [])
{
return $this->createClient(BigQueryClient::class, 'bigquery', $config);
}
/**
* 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:
* ```
* $datastore = $cloud->datastore();
* ```
*
* @param array $config [optional] {
* Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the other available options.
*
* @type bool $returnInt64AsObject If true, 64 bit integers will be
* returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
* @return DatastoreClient
*/
public function datastore(array $config = [])
{
return $this->createClient(DatastoreClient::class, 'datastore', $config);
}
/**
* Cloud Firestore is a flexible, scalable, realtime database for mobile,
* web, and server development. Find more information at the
* [Google Cloud firestore docs](https://cloud.google.com/firestore/docs/).
*
* Example:
* ```
* $firestore = $cloud->firestore();
* ```
*
* @param array $config [optional] {
* Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the other available options.
*
* @type bool $returnInt64AsObject If true, 64 bit integers will be
* returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
* @return FirestoreClient
*/
public function firestore(array $config = [])
{
return $this->createClient(FirestoreClient::class, 'firestore', $config);
}
/**
* 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:
* ```
* $logging = $cloud->logging();
* ```
*
* @param array $config [optional] Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the available options.
* @return LoggingClient
*/
public function logging(array $config = [])
{
return $this->createClient(LoggingClient::class, 'logging', $config);
}
/**
* 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:
* ```
* $language = $cloud->language();
* ```
*
* @param array $config [optional] Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the available options.
* @return LanguageServiceClient
*/
public function language(array $config = [])
{
if (class_exists(DeprecatedLanguageClient::class)) {
return $this->createClient(DeprecatedLanguageClient::class, 'vision', $config);
}
throw new \BadMethodCallException(sprintf(
'This method is no longer supported, create %s directly instead.',
LanguageServiceClient::class
));
}
/**
* 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:
* ```
* $pubsub = $cloud->pubsub(['projectId' => 'my-project']);
* ```
*
* @param array $config [optional] {
* Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the other available options.
*
* @type string $transport The transport type used for requests. May be
* either `grpc` or `rest`. **Defaults to** `grpc` if gRPC support
* is detected on the system.
* @return PubSubClient
*/
public function pubsub(array $config = [])
{
return $this->createClient(PubSubClient::class, 'pubsub', $config);
}
/**
* 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(['projectId' => 'my-project']);
* ```
*
* @param array $config [optional] {
* Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the other available options.
*
* @type bool $returnInt64AsObject If true, 64 bit integers will be
* returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
* }
* @return SpannerClient
*/
public function spanner(array $config = [])
{
return $this->createClient(SpannerClient::class, 'spanner', $config);
}
/**
* @deprecated
* @see SpeechClient
* @throws \BadMethodCallException
*/
public function speech(array $config = [])
{
if (class_exists(DeprecatedSpeechClient::class)) {
return $this->createClient(DeprecatedSpeechClient::class, 'speech', $config);
}
throw new \BadMethodCallException(sprintf(
'This method is no longer supported, create %s directly instead.',
SpeechClient::class
));
}
/**
* 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:
* ```
* $storage = $cloud->storage();
* ```
*
* @param array $config [optional] Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the available options.
* @return StorageClient
*/
public function storage(array $config = [])
{
return $this->createClient(StorageClient::class, 'storage', $config);
}
/**
* Google Stackdriver Trace allows you to collect latency data from your applications
* and display it in the Google Cloud Platform Console. Find more information at
* [Stackdriver Trace API docs](https://cloud.google.com/trace/docs/).
*
* Example:
* ```
* $trace = $cloud->trace();
* ```
*
* @param array $config [optional] Configuration options. See
* {@see \Google\Cloud\Core\ServiceBuilder::__construct()} for the available options.
* @return TraceClient
*/
public function trace(array $config = [])
{
return $this->createClient(TraceClient::class, 'trace', $config);
}
/**
* @deprecated
* @see ImageAnnotatorClient
* @throws \BadMethodCallException
*/
public function vision(array $config = [])
{
if (class_exists(DeprecatedVisionClient::class)) {
return $this->createClient(DeprecatedVisionClient::class, 'vision', $config);
}
throw new \BadMethodCallException(sprintf(
'This method is no longer supported, create %s directly instead.',
ImageAnnotatorClient::class
));
}
/**
* @deprecated
* @see TranslationServiceClient
* @throws \BadMethodCallException
*/
public function translate(array $config = [])
{
if (class_exists(DeprecatedTranslateClient::class)) {
return $this->createClient(DeprecatedTranslateClient::class, 'translate', $config);
}
throw new \BadMethodCallException(sprintf(
'This method is no longer supported, create %s directly instead.',
TranslationServiceClient::class
));
}
/**
* Create the client library, or error if not installed.
*
* @param string $class The class to create.
* @param string $packageName The name of the package
* @param array $config Configuration options.
*/
private function createClient($class, $packageName, array $config = [])
{
if (class_exists($class)) {
return new $class($this->resolveConfig($config + $this->config));
}
throw new \Exception(sprintf(
'The google/cloud-%s package is missing and must be installed.',
$packageName
));
}
/**
* Resolves configuration options.
*
* @param array $config
* @return array
*/
private function resolveConfig(array $config)
{
if (!isset($config['httpHandler'])) {
$config['httpHandler'] = HttpHandlerFactory::build();
}
if (!isset($config['asyncHttpHandler'])) {
$config['asyncHttpHandler'] = $config['httpHandler'] instanceof Guzzle6HttpHandler
? [$config['httpHandler'], 'async']
: [HttpHandlerFactory::build(), 'async'];
}
return array_merge($this->config, $config);
}
}

Some files were not shown because too many files have changed in this diff Show More