Skip to content

Commit

Permalink
Added a Marshaler helper object to the DynamoDB namespace that can ha…
Browse files Browse the repository at this point in the history
…ndle maps and lists.
  • Loading branch information
jeremeamia committed Nov 25, 2014
1 parent 26df032 commit 0a9c4fd
Show file tree
Hide file tree
Showing 4 changed files with 518 additions and 3 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,12 @@
# CHANGELOG

## Unreleased

* Added a DynamoDB `Marshaler` class, that allows you to marshal JSON documents
or native PHP arrays to the format that DynamoDB requires. You can also
unmarshal item data from operation results back into JSON documents or native
PHP arrays.

## 2.7.6 - 2014-11-20

* Added support for AWS KMS integration to the Amazon Redshift Client.
Expand Down
173 changes: 173 additions & 0 deletions src/Aws/DynamoDb/Marshaler.php
@@ -0,0 +1,173 @@
<?php
namespace Aws\DynamoDb;

/**
* Marshals JSON documents or array representations of JSON documents into the
* parameter structure required by DynamoDB. Also allows for unmarshaling. Does
* not support binary (B) or set (*S) types, since they are not supported
* explicitly in JSON.
*/
class Marshaler
{
/**
* Marshal a JSON document from a string to an array that is formatted in
* the proper parameter structure required by DynamoDB operations.
*
* @param string $json A valid JSON document.
*
* @return array
* @throws \InvalidArgumentException if the JSON is invalid.
*/
public function marshalJson($json)
{
$data = json_decode($json);
if (!($data instanceof \stdClass)) {
throw new \InvalidArgumentException(
'The JSON document must be valid and be an object at its root.'
);
}

return current($this->marshalValue($data));
}

/**
* Marshal a native PHP array of data to a new array that is formatted in
* the proper parameter structure required by DynamoDB operations.
*
* @param array|\stdClass $item An associative array of data.
*
* @return array
*/
public function marshalItem($item)
{
return current($this->marshalValue($item));
}

/**
* Marshal a native PHP value into an array that is formatted in the proper
* parameter structure required by DynamoDB operations.
*
* @param mixed $value A scalar, array, or stdClass value.
*
* @return array Formatted like `array(TYPE => VALUE)`.
* @throws \UnexpectedValueException if the value cannot be marshaled.
*/
private function marshalValue($value)
{
$type = gettype($value);
if ($type === 'string' && $value !== '') {
$type = 'S';
} elseif ($type === 'integer' || $type === 'double') {
$type = 'N';
$value = (string) $value;
} elseif ($type === 'boolean') {
$type = 'BOOL';
} elseif ($type === 'NULL') {
$type = 'NULL';
$value = true;
} elseif ($type === 'array'
|| $value instanceof \Traversable
|| $value instanceof \stdClass
) {
$type = $value instanceof \stdClass ? 'M' : 'L';
$data = array();
$expectedIndex = -1;
foreach ($value as $k => $v) {
$data[$k] = $this->marshalValue($v);
if ($type === 'L' && (!is_int($k) || $k != ++$expectedIndex)) {
$type = 'M';
}
}
$value = $data;
} else {
$type = $type === 'object' ? get_class($value) : $type;
throw new \UnexpectedValueException('Marshaling error: ' . ($value
? "encountered unexpected type \"{$type}\"."
: 'encountered empty value.'
));
}

return array($type => $value);
}

/**
* Unmarshal a document (item) from a DynamoDB operation result into a JSON
* document string.
*
* @param array $data Item/document from a DynamoDB result.
* @param int $jsonEncodeFlags Flags to use with `json_encode()`.
*
* @return string
*/
public function unmarshalJson(array $data, $jsonEncodeFlags = 0)
{
return json_encode(
$this->unmarshalValue(array('M' => $data), true),
$jsonEncodeFlags
);
}

/**
* Unmarshal an item from a DynamoDB operation result into a native PHP
* array. If you set $mapAsObject to true, then a stdClass value will be
* returned instead.
*
* @param array $data Item from a DynamoDB result.
*
* @return array|\stdClass
*/
public function unmarshalItem(array $data)
{
return $this->unmarshalValue(array('M' => $data));
}

/**
* Unmarshal a value from a DynamoDB operation result into a native PHP
* value. Will return a scalar, array, or (if you set $mapAsObject to true)
* stdClass value.
*
* @param array $value Value from a DynamoDB result.
* @param bool $mapAsObject Whether maps should be represented as stdClass.
*
* @return mixed
* @throws \UnexpectedValueException
*/
private function unmarshalValue(array $value, $mapAsObject = false)
{
list($type, $value) = each($value);
switch ($type) {
case 'S':
case 'SS':
case 'B':
case 'BS':
case 'BOOL':
return $value;
case 'NULL':
return null;
case 'N':
// Use type coercion to unmarshal numbers to int/float.
return $value + 0;
case 'NS':
foreach ($value as &$v) {
$v += 0;
}
return $value;
case 'M':
if ($mapAsObject) {
$data = new \stdClass;
foreach ($value as $k => $v) {
$data->$k = $this->unmarshalValue($v, $mapAsObject);
}
return $data;
}
// Else, unmarshal M the same way as L.
case 'L':
foreach ($value as &$v) {
$v = $this->unmarshalValue($v, $mapAsObject);
}
return $value;
}

throw new \UnexpectedValueException("Unexpected type: {$type}.");
}
}
42 changes: 39 additions & 3 deletions tests/Aws/Tests/DynamoDb/Integration/DynamoDb_20120810_Test.php
Expand Up @@ -18,6 +18,7 @@

use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\Iterator\ItemIterator;
use Aws\DynamoDb\Marshaler;

/**
* @group example
Expand Down Expand Up @@ -203,22 +204,22 @@ public function testListTablesWithIterator()
}

/**
* Put an item in a table using the formatAttributes() client helper method
* Put an item in a table using the marshaler helper
*
* @depends testListTablesWithIterator
* @example Aws\DynamoDb\DynamoDbClient::putItem 2012-08-10
* @example Aws\DynamoDb\DynamoDbClient::formatAttributes 2012-08-10
*/
public function testAddItem()
{
$client = $this->client;
// @begin

$m = new Marshaler();
$time = time();

$result = $client->putItem(array(
'TableName' => 'errors',
'Item' => $client->formatAttributes(array(
'Item' => $m->marshalItem(array(
'id' => 1201,
'time' => $time,
'error' => 'Executive overflow',
Expand Down Expand Up @@ -572,6 +573,41 @@ public function testDeleteItem()
}
}

/**
* Put a JSON document in a table using the marshaler helper
*
* @depends testWaitUntilTableExists
*/
public function testMarshaler()
{
$client = $this->client;
// @begin

$m = new Marshaler();
$time = time();
$json = '{"id":1500,"time":'.$time.',"people":[{"name":"Jim","alive":tr'
. 'ue},{"name":"Bob","alive":false},{"name":"Amy","alive":true}]}';

$client->putItem(array(
'TableName' => 'errors',
'Item' => $m->marshalJson($json)
));

$result = $client->getItem(array(
'TableName' => 'errors',
'Key' => $m->marshalItem(array(
'id' => 1500,
'time' => $time
))
));

// @end
$this->assertEquals(
json_decode($json, true),
json_decode($m->unmarshalJson($result['Item']), true)
);
}

/**
* Delete a table
*
Expand Down

0 comments on commit 0a9c4fd

Please sign in to comment.