<?php
/*
* Copyright 2017 Google LLC
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace Google\ApiCore;
use Google\Protobuf\Any;
use Google\Protobuf\Descriptor;
use Google\Protobuf\DescriptorPool;
use Google\Protobuf\FieldDescriptor;
use Google\Protobuf\Internal\Message;
use RuntimeException;
/**
* Collection of methods to help with serialization of protobuf objects
*/
class Serializer
{
const MAP_KEY_FIELD_NAME = 'key';
const MAP_VALUE_FIELD_NAME = 'value';
private static $phpArraySerializer;
private static $metadataKnownTypes = [
'google.rpc.retryinfo-bin' => \Google\Rpc\RetryInfo::class,
'google.rpc.debuginfo-bin' => \Google\Rpc\DebugInfo::class,
'google.rpc.quotafailure-bin' => \Google\Rpc\QuotaFailure::class,
'google.rpc.badrequest-bin' => \Google\Rpc\BadRequest::class,
'google.rpc.requestinfo-bin' => \Google\Rpc\RequestInfo::class,
'google.rpc.resourceinfo-bin' => \Google\Rpc\ResourceInfo::class,
'google.rpc.help-bin' => \Google\Rpc\Help::class,
'google.rpc.localizedmessage-bin' => \Google\Rpc\LocalizedMessage::class,
];
private $fieldTransformers;
private $messageTypeTransformers;
private $decodeFieldTransformers;
private $decodeMessageTypeTransformers;
private $descriptorMaps = [];
/**
* Serializer constructor.
*
* @param array $fieldTransformers An array mapping field names to transformation functions
* @param array $messageTypeTransformers An array mapping message names to transformation functions
* @param array $decodeFieldTransformers An array mapping field names to transformation functions
* @param array $decodeMessageTypeTransformers An array mapping message names to transformation functions
*/
public function __construct(
$fieldTransformers = [],
$messageTypeTransformers = [],
$decodeFieldTransformers = [],
$decodeMessageTypeTransformers = []
) {
$this->fieldTransformers = $fieldTransformers;
$this->messageTypeTransformers = $messageTypeTransformers;
$this->decodeFieldTransformers = $decodeFieldTransformers;
$this->decodeMessageTypeTransformers = $decodeMessageTypeTransformers;
}
/**
* Encode protobuf message as a PHP array
*
* @param mixed $message
* @return array
* @throws ValidationException
*/
public function encodeMessage($message)
{
// Get message descriptor
$pool = DescriptorPool::getGeneratedPool();
$messageType = $pool->getDescriptorByClassName(get_class($message));
try {
return $this->encodeMessageImpl($message, $messageType);
} catch (\Exception $e) {
throw new ValidationException(
"Error encoding message: " . $e->getMessage(),
$e->getCode(),
$e
);
}
}
/**
* Decode PHP array into the specified protobuf message
*
* @param mixed $message
* @param array $data
* @return mixed
* @throws ValidationException
*/
public function decodeMessage($message, $data)
{
// Get message descriptor
$pool = DescriptorPool::getGeneratedPool();
$messageType = $pool->getDescriptorByClassName(get_class($message));
try {
return $this->decodeMessageImpl($message, $messageType, $data);
} catch (\Exception $e) {
throw new ValidationException(
"Error decoding message: " . $e->getMessage(),
$e->getCode(),
$e
);
}
}
/**
* @param Message $message
* @return string Json representation of $message
* @throws ValidationException
*/
public static function serializeToJson($message)
{
return json_encode(self::serializeToPhpArray($message), JSON_PRETTY_PRINT);
}
/**
* @param Message $message
* @return array PHP array representation of $message
* @throws ValidationException
*/
public static function serializeToPhpArray($message)
{
return self::getPhpArraySerializer()->encodeMessage($message);
}
/**
* Decode metadata received from gRPC status object
*
* @param array $metadata
* @return array
*/
public static function decodeMetadata($metadata)
{
if (is_null($metadata) || count($metadata) == 0) {
return [];
}
$result = [];
foreach ($metadata as $key => $values) {
foreach ($values as $value) {
$decodedValue = [
'@type' => $key,
];
if (self::hasBinaryHeaderSuffix($key)) {
if (isset(self::$metadataKnownTypes[$key])) {
$class = self::$metadataKnownTypes[$key];
/** @var Message $message */
$message = new $class();
try {
$message->mergeFromString($value);
$decodedValue += self::serializeToPhpArray($message);
} catch (\Exception $e) {
// We encountered an error trying to deserialize the data
$decodedValue += [
'data' => '<Unable to deserialize data>',
];
}
} else {
// The metadata contains an unexpected binary type
$decodedValue += [
'data' => '<Unknown Binary Data>',
];
}
} else {
$decodedValue += [
'data' => $value,
];
}
$result[] = $decodedValue;
}
}
return $result;
}
/**
* Decode an array of Any messages into a printable PHP array.
*
* @param $anyArray
* @return array
*/
public static function decodeAnyMessages($anyArray)
{
$results = [];
foreach ($anyArray as $any) {
try {
/** @var Any $any */
/** @var Message $unpacked */
$unpacked = $any->unpack();
$results[] = self::serializeToPhpArray($unpacked);
} catch (\Exception $ex) {
echo "$ex\n";
// failed to unpack the $any object - show as unknown binary data
$results[] = [
'typeUrl' => $any->getTypeUrl(),
'value' => '<Unknown Binary Data>',
];
}
}
return $results;
}
/**
* @param FieldDescriptor $field
* @param $data
* @return mixed array
* @throws \Exception
*/
private function encodeElement(FieldDescriptor $field, $data)
{
switch ($field->getType()) {
case GPBType::MESSAGE:
if (is_array($data)) {
$result = $data;
} else {
$result = $this->encodeMessageImpl($data, $field->getMessageType());
}
$messageType = $field->getMessageType()->getFullName();
if (isset($this->messageTypeTransformers[$messageType])) {
$result = $this->messageTypeTransformers[$messageType]($result);
}
break;
default:
$result = $data;
break;
}
if (isset($this->fieldTransformers[$field->getName()])) {
$result = $this->fieldTransformers[$field->getName()]($result);
}
return $result;
}
private function getDescriptorMaps(Descriptor $descriptor)
{
if (!isset($this->descriptorMaps[$descriptor->getFullName()])) {
$fieldsByName = [];
$fieldCount = $descriptor->getFieldCount();
for ($i = 0; $i < $fieldCount; $i++) {
$field = $descriptor->getField($i);
$fieldsByName[$field->getName()] = $field;
}
$fieldToOneof = [];
$oneofCount = $descriptor->getOneofDeclCount();
for ($i = 0; $i < $oneofCount; $i++) {
$oneof = $descriptor->getOneofDecl($i);
$oneofFieldCount = $oneof->getFieldCount();
for ($j = 0; $j < $oneofFieldCount; $j++) {
$field = $oneof->getField($j);
$fieldToOneof[$field->getName()] = $oneof->getName();
}
}
$this->descriptorMaps[$descriptor->getFullName()] = [$fieldsByName, $fieldToOneof];
}
return $this->descriptorMaps[$descriptor->getFullName()];
}
/**
* @param Message $message
* @param Descriptor $messageType
* @return array
* @throws \Exception
*/
private function encodeMessageImpl($message, Descriptor $messageType)
{
$data = [];
$fieldCount = $messageType->getFieldCount();
for ($i = 0; $i < $fieldCount; $i++) {
$field = $messageType->getField($i);
$key = $field->getName();
$getter = $this->getGetter($key);
$v = $message->$getter();
if (is_null($v)) {
continue;
}
// Check and skip unset fields inside oneofs
list($_, $fieldsToOneof) = $this->getDescriptorMaps($messageType);
if (isset($fieldsToOneof[$key])) {
$oneofName = $fieldsToOneof[$key];
$oneofGetter = $this->getGetter($oneofName);
if ($message->$oneofGetter() !== $key) {
continue;
}
}
if ($field->isMap()) {
list($mapFieldsByName, $_) = $this->getDescriptorMaps($field->getMessageType());
$keyField = $mapFieldsByName[self::MAP_KEY_FIELD_NAME];
$valueField = $mapFieldsByName[self::MAP_VALUE_FIELD_NAME];
$arr = [];
foreach ($v as $k => $vv) {
$arr[$this->encodeElement($keyField, $k)] = $this->encodeElement($valueField, $vv);
}
$v = $arr;
} elseif ($field->getLabel() === GPBLabel::REPEATED) {
$arr = [];
foreach ($v as $k => $vv) {
$arr[$k] = $this->encodeElement($field, $vv);
}
$v = $arr;
} else {
$v = $this->encodeElement($field, $v);
}
$key = self::toCamelCase($key);
$data[$key] = $v;
}
return $data;
}
/**
* @param FieldDescriptor $field
* @param mixed $data
* @return mixed
* @throws \Exception
*/
private function decodeElement(FieldDescriptor $field, $data)
{
if (isset($this->decodeFieldTransformers[$field->getName()])) {
$data = $this->decodeFieldTransformers[$field->getName()]($data);
}
switch ($field->getType()) {
case GPBType::MESSAGE:
if ($data instanceof Message) {
return $data;
}
$messageType = $field->getMessageType();
$messageTypeName = $messageType->getFullName();
$klass = $messageType->getClass();
$msg = new $klass();
if (isset($this->decodeMessageTypeTransformers[$messageTypeName])) {
$data = $this->decodeMessageTypeTransformers[$messageTypeName]($data);
}
return $this->decodeMessageImpl($msg, $messageType, $data);
default:
return $data;
}
}
/**
* @param Message $message
* @param Descriptor $messageType
* @param array $data
* @return mixed
* @throws \Exception
*/
private function decodeMessageImpl($message, Descriptor $messageType, $data)
{
list($fieldsByName, $_) = $this->getDescriptorMaps($messageType);
foreach ($data as $key => $v) {
// Get the field by tag number or name
$fieldName = self::toSnakeCase($key);
// Unknown field found
if (!isset($fieldsByName[$fieldName])) {
throw new RuntimeException(sprintf(
"cannot handle unknown field %s on message %s",
$fieldName,
$messageType->getFullName()
));
}
/** @var $field FieldDescriptor */
$field = $fieldsByName[$fieldName];
if ($field->isMap()) {
list($mapFieldsByName, $_) = $this->getDescriptorMaps($field->getMessageType());
$keyField = $mapFieldsByName[self::MAP_KEY_FIELD_NAME];
$valueField = $mapFieldsByName[self::MAP_VALUE_FIELD_NAME];
$arr = [];
foreach ($v as $k => $vv) {
$arr[$this->decodeElement($keyField, $k)] = $this->decodeElement($valueField, $vv);
}
$value = $arr;
} elseif ($field->getLabel() === GPBLabel::REPEATED) {
$arr = [];
foreach ($v as $k => $vv) {
$arr[$k] = $this->decodeElement($field, $vv);
}
$value = $arr;
} else {
$value = $this->decodeElement($field, $v);
}
$setter = $this->getSetter($field->getName());
$message->$setter($value);
// We must unset $value here, otherwise the protobuf c extension will mix up the references
// and setting one value will change all others
unset($value);
}
return $message;
}
/**
* @param string $name
* @return string Getter function
*/
public static function getGetter($name)
{
return 'get' . ucfirst(self::toCamelCase($name));
}
/**
* @param string $name
* @return string Setter function
*/
public static function getSetter($name)
{
return 'set' . ucfirst(self::toCamelCase($name));
}
/**
* Convert string from camelCase to snake_case
*
* @param string $key
* @return string
*/
public static function toSnakeCase($key)
{
return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $key));
}
/**
* Convert string from snake_case to camelCase
*
* @param string $key
* @return string
*/
public static function toCamelCase($key)
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
}
private static function hasBinaryHeaderSuffix($key)
{
return substr_compare($key, "-bin", strlen($key) - 4) === 0;
}
private static function getPhpArraySerializer()
{
if (is_null(self::$phpArraySerializer)) {
self::$phpArraySerializer = new Serializer();
}
return self::$phpArraySerializer;
}
public static function loadKnownMetadataTypes()
{
foreach (self::$metadataKnownTypes as $key => $class) {
new $class;
}
}
}
// It is necessary to call this when this file is included. Otherwise we cannot be
// guaranteed that the relevant classes will be loaded into the protobuf descriptor
// pool when we try to unpack an Any object containing that class.
// phpcs:disable PSR1.Files.SideEffects
Serializer::loadKnownMetadataTypes();
// phpcs:enable