| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | namespace Aws\DynamoDb;
 | 
        
           |  |  | 3 |   | 
        
           |  |  | 4 | use Psr\Http\Message\StreamInterface;
 | 
        
           |  |  | 5 |   | 
        
           |  |  | 6 | /**
 | 
        
           |  |  | 7 |  * Marshals and unmarshals JSON documents and PHP arrays into DynamoDB items.
 | 
        
           |  |  | 8 |  */
 | 
        
           |  |  | 9 | class Marshaler
 | 
        
           |  |  | 10 | {
 | 
        
           |  |  | 11 |     /** @var array Default options to merge into provided options. */
 | 
        
           |  |  | 12 |     private static $defaultOptions = [
 | 
        
           |  |  | 13 |         'ignore_invalid'  => false,
 | 
        
           |  |  | 14 |         'nullify_invalid' => false,
 | 
        
           |  |  | 15 |         'wrap_numbers'    => false,
 | 
        
           |  |  | 16 |     ];
 | 
        
           |  |  | 17 |   | 
        
           |  |  | 18 |     /** @var array Marshaler options. */
 | 
        
           |  |  | 19 |     private $options;
 | 
        
           |  |  | 20 |   | 
        
           |  |  | 21 |     /**
 | 
        
           |  |  | 22 |      * Instantiates a DynamoDB Marshaler.
 | 
        
           |  |  | 23 |      *
 | 
        
           |  |  | 24 |      * The following options are valid.
 | 
        
           |  |  | 25 |      *
 | 
        
           |  |  | 26 |      * - ignore_invalid: (bool) Set to `true` if invalid values should be
 | 
        
           |  |  | 27 |      *   ignored (i.e., not included) during marshaling.
 | 
        
           |  |  | 28 |      * - nullify_invalid: (bool) Set to `true` if invalid values should be set
 | 
        
           |  |  | 29 |      *   to null.
 | 
        
           |  |  | 30 |      * - wrap_numbers: (bool) Set to `true` to wrap numbers with `NumberValue`
 | 
        
           |  |  | 31 |      *   objects during unmarshaling to preserve the precision.
 | 
        
           |  |  | 32 |      *
 | 
        
           |  |  | 33 |      * @param array $options Marshaler options
 | 
        
           |  |  | 34 |      */
 | 
        
           |  |  | 35 |     public function __construct(array $options = [])
 | 
        
           |  |  | 36 |     {
 | 
        
           |  |  | 37 |         $this->options = $options + self::$defaultOptions;
 | 
        
           |  |  | 38 |     }
 | 
        
           |  |  | 39 |   | 
        
           |  |  | 40 |     /**
 | 
        
           |  |  | 41 |      * Creates a special object to represent a DynamoDB binary (B) value.
 | 
        
           |  |  | 42 |      *
 | 
        
           |  |  | 43 |      * This helps disambiguate binary values from string (S) values.
 | 
        
           |  |  | 44 |      *
 | 
        
           |  |  | 45 |      * @param mixed $value A binary value compatible with Guzzle streams.
 | 
        
           |  |  | 46 |      *
 | 
        
           |  |  | 47 |      * @return BinaryValue
 | 
        
           |  |  | 48 |      * @see GuzzleHttp\Stream\Stream::factory
 | 
        
           |  |  | 49 |      */
 | 
        
           |  |  | 50 |     public function binary($value)
 | 
        
           |  |  | 51 |     {
 | 
        
           |  |  | 52 |         return new BinaryValue($value);
 | 
        
           |  |  | 53 |     }
 | 
        
           |  |  | 54 |   | 
        
           |  |  | 55 |     /**
 | 
        
           |  |  | 56 |      * Creates a special object to represent a DynamoDB number (N) value.
 | 
        
           |  |  | 57 |      *
 | 
        
           |  |  | 58 |      * This helps maintain the precision of large integer/float in PHP.
 | 
        
           |  |  | 59 |      *
 | 
        
           |  |  | 60 |      * @param string|int|float $value A number value.
 | 
        
           |  |  | 61 |      *
 | 
        
           |  |  | 62 |      * @return NumberValue
 | 
        
           |  |  | 63 |      */
 | 
        
           |  |  | 64 |     public function number($value)
 | 
        
           |  |  | 65 |     {
 | 
        
           |  |  | 66 |         return new NumberValue($value);
 | 
        
           |  |  | 67 |     }
 | 
        
           |  |  | 68 |   | 
        
           |  |  | 69 |     /**
 | 
        
           |  |  | 70 |      * Creates a special object to represent a DynamoDB set (SS/NS/BS) value.
 | 
        
           |  |  | 71 |      *
 | 
        
           |  |  | 72 |      * This helps disambiguate set values from list (L) values.
 | 
        
           |  |  | 73 |      *
 | 
        
           |  |  | 74 |      * @param array $values The values of the set.
 | 
        
           |  |  | 75 |      *
 | 
        
           |  |  | 76 |      * @return SetValue
 | 
        
           |  |  | 77 |      *
 | 
        
           |  |  | 78 |      */
 | 
        
           |  |  | 79 |     public function set(array $values)
 | 
        
           |  |  | 80 |     {
 | 
        
           |  |  | 81 |         return new SetValue($values);
 | 
        
           |  |  | 82 |     }
 | 
        
           |  |  | 83 |   | 
        
           |  |  | 84 |     /**
 | 
        
           |  |  | 85 |      * Marshal a JSON document from a string to a DynamoDB item.
 | 
        
           |  |  | 86 |      *
 | 
        
           |  |  | 87 |      * The result is an array formatted in the proper parameter structure
 | 
        
           |  |  | 88 |      * required by the DynamoDB API for items.
 | 
        
           |  |  | 89 |      *
 | 
        
           |  |  | 90 |      * @param string $json A valid JSON document.
 | 
        
           |  |  | 91 |      *
 | 
        
           |  |  | 92 |      * @return array Item formatted for DynamoDB.
 | 
        
           |  |  | 93 |      * @throws \InvalidArgumentException if the JSON is invalid.
 | 
        
           |  |  | 94 |      */
 | 
        
           |  |  | 95 |     public function marshalJson($json)
 | 
        
           |  |  | 96 |     {
 | 
        
           |  |  | 97 |         $data = json_decode($json);
 | 
        
           |  |  | 98 |         if (!($data instanceof \stdClass)) {
 | 
        
           |  |  | 99 |             throw new \InvalidArgumentException(
 | 
        
           |  |  | 100 |                 'The JSON document must be valid and be an object at its root.'
 | 
        
           |  |  | 101 |             );
 | 
        
           |  |  | 102 |         }
 | 
        
           |  |  | 103 |   | 
        
           |  |  | 104 |         return current($this->marshalValue($data));
 | 
        
           |  |  | 105 |     }
 | 
        
           |  |  | 106 |   | 
        
           |  |  | 107 |     /**
 | 
        
           |  |  | 108 |      * Marshal a native PHP array of data to a DynamoDB item.
 | 
        
           |  |  | 109 |      *
 | 
        
           |  |  | 110 |      * The result is an array formatted in the proper parameter structure
 | 
        
           |  |  | 111 |      * required by the DynamoDB API for items.
 | 
        
           |  |  | 112 |      *
 | 
        
           |  |  | 113 |      * @param array|\stdClass $item An associative array of data.
 | 
        
           |  |  | 114 |      *
 | 
        
           |  |  | 115 |      * @return array Item formatted for DynamoDB.
 | 
        
           |  |  | 116 |      */
 | 
        
           |  |  | 117 |     public function marshalItem($item)
 | 
        
           |  |  | 118 |     {
 | 
        
           |  |  | 119 |         return current($this->marshalValue($item));
 | 
        
           |  |  | 120 |     }
 | 
        
           |  |  | 121 |   | 
        
           |  |  | 122 |     /**
 | 
        
           |  |  | 123 |      * Marshal a native PHP value into a DynamoDB attribute value.
 | 
        
           |  |  | 124 |      *
 | 
        
           |  |  | 125 |      * The result is an associative array that is formatted in the proper
 | 
        
           |  |  | 126 |      * `[TYPE => VALUE]` parameter structure required by the DynamoDB API.
 | 
        
           |  |  | 127 |      *
 | 
        
           |  |  | 128 |      * @param mixed $value A scalar, array, or `stdClass` value.
 | 
        
           |  |  | 129 |      *
 | 
        
           |  |  | 130 |      * @return array Attribute formatted for DynamoDB.
 | 
        
           |  |  | 131 |      * @throws \UnexpectedValueException if the value cannot be marshaled.
 | 
        
           |  |  | 132 |      */
 | 
        
           |  |  | 133 |     public function marshalValue($value)
 | 
        
           |  |  | 134 |     {
 | 
        
           |  |  | 135 |         $type = gettype($value);
 | 
        
           |  |  | 136 |   | 
        
           |  |  | 137 |         // Handle string values.
 | 
        
           |  |  | 138 |         if ($type === 'string') {
 | 
        
           |  |  | 139 |             return ['S' => $value];
 | 
        
           |  |  | 140 |         }
 | 
        
           |  |  | 141 |   | 
        
           |  |  | 142 |         // Handle number values.
 | 
        
           |  |  | 143 |         if ($type === 'integer'
 | 
        
           |  |  | 144 |             || $type === 'double'
 | 
        
           |  |  | 145 |             || $value instanceof NumberValue
 | 
        
           |  |  | 146 |         ) {
 | 
        
           |  |  | 147 |             return ['N' => (string) $value];
 | 
        
           |  |  | 148 |         }
 | 
        
           |  |  | 149 |   | 
        
           |  |  | 150 |         // Handle boolean values.
 | 
        
           |  |  | 151 |         if ($type === 'boolean') {
 | 
        
           |  |  | 152 |             return ['BOOL' => $value];
 | 
        
           |  |  | 153 |         }
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 |         // Handle null values.
 | 
        
           |  |  | 156 |         if ($type === 'NULL') {
 | 
        
           |  |  | 157 |             return ['NULL' => true];
 | 
        
           |  |  | 158 |         }
 | 
        
           |  |  | 159 |   | 
        
           |  |  | 160 |         // Handle set values.
 | 
        
           |  |  | 161 |         if ($value instanceof SetValue) {
 | 
        
           |  |  | 162 |             if (count($value) === 0) {
 | 
        
           |  |  | 163 |                 return $this->handleInvalid('empty sets are invalid');
 | 
        
           |  |  | 164 |             }
 | 
        
           |  |  | 165 |             $previousType = null;
 | 
        
           |  |  | 166 |             $data = [];
 | 
        
           |  |  | 167 |             foreach ($value as $v) {
 | 
        
           |  |  | 168 |                 $marshaled = $this->marshalValue($v);
 | 
        
           |  |  | 169 |                 $setType = key($marshaled);
 | 
        
           |  |  | 170 |                 if (!$previousType) {
 | 
        
           |  |  | 171 |                     $previousType = $setType;
 | 
        
           |  |  | 172 |                 } elseif ($setType !== $previousType) {
 | 
        
           |  |  | 173 |                     return $this->handleInvalid('sets must be uniform in type');
 | 
        
           |  |  | 174 |                 }
 | 
        
           |  |  | 175 |                 $data[] = current($marshaled);
 | 
        
           |  |  | 176 |             }
 | 
        
           |  |  | 177 |   | 
        
           |  |  | 178 |             return [$previousType . 'S' => array_values(array_unique($data))];
 | 
        
           |  |  | 179 |         }
 | 
        
           |  |  | 180 |   | 
        
           |  |  | 181 |         // Handle list and map values.
 | 
        
           |  |  | 182 |         $dbType = 'L';
 | 
        
           |  |  | 183 |         if ($value instanceof \stdClass) {
 | 
        
           |  |  | 184 |             $type = 'array';
 | 
        
           |  |  | 185 |             $dbType = 'M';
 | 
        
           |  |  | 186 |         }
 | 
        
           |  |  | 187 |         if ($type === 'array' || $value instanceof \Traversable) {
 | 
        
           |  |  | 188 |             $data = [];
 | 
        
           |  |  | 189 |             $index = 0;
 | 
        
           |  |  | 190 |             foreach ($value as $k => $v) {
 | 
        
           |  |  | 191 |                 if ($v = $this->marshalValue($v)) {
 | 
        
           |  |  | 192 |                     $data[$k] = $v;
 | 
        
           |  |  | 193 |                     if ($dbType === 'L' && (!is_int($k) || $k != $index++)) {
 | 
        
           |  |  | 194 |                         $dbType = 'M';
 | 
        
           |  |  | 195 |                     }
 | 
        
           |  |  | 196 |                 }
 | 
        
           |  |  | 197 |             }
 | 
        
           |  |  | 198 |             return [$dbType => $data];
 | 
        
           |  |  | 199 |         }
 | 
        
           |  |  | 200 |   | 
        
           |  |  | 201 |         // Handle binary values.
 | 
        
           |  |  | 202 |         if (is_resource($value) || $value instanceof StreamInterface) {
 | 
        
           |  |  | 203 |             $value = $this->binary($value);
 | 
        
           |  |  | 204 |         }
 | 
        
           |  |  | 205 |         if ($value instanceof BinaryValue) {
 | 
        
           |  |  | 206 |             return ['B' => (string) $value];
 | 
        
           |  |  | 207 |         }
 | 
        
           |  |  | 208 |   | 
        
           |  |  | 209 |         // Handle invalid values.
 | 
        
           |  |  | 210 |         return $this->handleInvalid('encountered unexpected value');
 | 
        
           |  |  | 211 |     }
 | 
        
           |  |  | 212 |   | 
        
           |  |  | 213 |     /**
 | 
        
           |  |  | 214 |      * Unmarshal a document (item) from a DynamoDB operation result into a JSON
 | 
        
           |  |  | 215 |      * document string.
 | 
        
           |  |  | 216 |      *
 | 
        
           |  |  | 217 |      * @param array $data            Item/document from a DynamoDB result.
 | 
        
           |  |  | 218 |      * @param int   $jsonEncodeFlags Flags to use with `json_encode()`.
 | 
        
           |  |  | 219 |      *
 | 
        
           |  |  | 220 |      * @return string
 | 
        
           |  |  | 221 |      */
 | 
        
           |  |  | 222 |     public function unmarshalJson(array $data, $jsonEncodeFlags = 0)
 | 
        
           |  |  | 223 |     {
 | 
        
           |  |  | 224 |         return json_encode(
 | 
        
           |  |  | 225 |             $this->unmarshalValue(['M' => $data], true),
 | 
        
           |  |  | 226 |             $jsonEncodeFlags
 | 
        
           |  |  | 227 |         );
 | 
        
           |  |  | 228 |     }
 | 
        
           |  |  | 229 |   | 
        
           |  |  | 230 |     /**
 | 
        
           |  |  | 231 |      * Unmarshal an item from a DynamoDB operation result into a native PHP
 | 
        
           |  |  | 232 |      * array. If you set $mapAsObject to true, then a stdClass value will be
 | 
        
           |  |  | 233 |      * returned instead.
 | 
        
           |  |  | 234 |      *
 | 
        
           |  |  | 235 |      * @param array $data Item from a DynamoDB result.
 | 
        
           |  |  | 236 |      * @param bool  $mapAsObject Whether maps should be represented as stdClass.
 | 
        
           |  |  | 237 |      *
 | 
        
           |  |  | 238 |      * @return array|\stdClass
 | 
        
           |  |  | 239 |      */
 | 
        
           |  |  | 240 |     public function unmarshalItem(array $data, $mapAsObject = false)
 | 
        
           |  |  | 241 |     {
 | 
        
           |  |  | 242 |         return $this->unmarshalValue(['M' => $data], $mapAsObject);
 | 
        
           |  |  | 243 |     }
 | 
        
           |  |  | 244 |   | 
        
           |  |  | 245 |     /**
 | 
        
           |  |  | 246 |      * Unmarshal a value from a DynamoDB operation result into a native PHP
 | 
        
           |  |  | 247 |      * value. Will return a scalar, array, or (if you set $mapAsObject to true)
 | 
        
           |  |  | 248 |      * stdClass value.
 | 
        
           |  |  | 249 |      *
 | 
        
           |  |  | 250 |      * @param array $value       Value from a DynamoDB result.
 | 
        
           |  |  | 251 |      * @param bool  $mapAsObject Whether maps should be represented as stdClass.
 | 
        
           |  |  | 252 |      *
 | 
        
           |  |  | 253 |      * @return mixed
 | 
        
           |  |  | 254 |      * @throws \UnexpectedValueException
 | 
        
           |  |  | 255 |      */
 | 
        
           |  |  | 256 |     public function unmarshalValue(array $value, $mapAsObject = false)
 | 
        
           |  |  | 257 |     {
 | 
        
           |  |  | 258 |         $type = key($value);
 | 
        
           |  |  | 259 |         $value = $value[$type];
 | 
        
           |  |  | 260 |         switch ($type) {
 | 
        
           |  |  | 261 |             case 'S':
 | 
        
           |  |  | 262 |             case 'BOOL':
 | 
        
           |  |  | 263 |                 return $value;
 | 
        
           |  |  | 264 |             case 'NULL':
 | 
        
           |  |  | 265 |                 return null;
 | 
        
           |  |  | 266 |             case 'N':
 | 
        
           |  |  | 267 |                 if ($this->options['wrap_numbers']) {
 | 
        
           |  |  | 268 |                     return new NumberValue($value);
 | 
        
           |  |  | 269 |                 }
 | 
        
           |  |  | 270 |   | 
        
           |  |  | 271 |                 // Use type coercion to unmarshal numbers to int/float.
 | 
        
           |  |  | 272 |                 return $value + 0;
 | 
        
           |  |  | 273 |             case 'M':
 | 
        
           |  |  | 274 |                 if ($mapAsObject) {
 | 
        
           |  |  | 275 |                     $data = new \stdClass;
 | 
        
           |  |  | 276 |                     foreach ($value as $k => $v) {
 | 
        
           |  |  | 277 |                         $data->$k = $this->unmarshalValue($v, $mapAsObject);
 | 
        
           |  |  | 278 |                     }
 | 
        
           |  |  | 279 |                     return $data;
 | 
        
           |  |  | 280 |                 }
 | 
        
           |  |  | 281 |                 // NOBREAK: Unmarshal M the same way as L, for arrays.
 | 
        
           |  |  | 282 |             case 'L':
 | 
        
           |  |  | 283 |                 foreach ($value as $k => $v) {
 | 
        
           |  |  | 284 |                     $value[$k] = $this->unmarshalValue($v, $mapAsObject);
 | 
        
           |  |  | 285 |                 }
 | 
        
           |  |  | 286 |                 return $value;
 | 
        
           |  |  | 287 |             case 'B':
 | 
        
           |  |  | 288 |                 return new BinaryValue($value);
 | 
        
           |  |  | 289 |             case 'SS':
 | 
        
           |  |  | 290 |             case 'NS':
 | 
        
           |  |  | 291 |             case 'BS':
 | 
        
           |  |  | 292 |                 foreach ($value as $k => $v) {
 | 
        
           |  |  | 293 |                     $value[$k] = $this->unmarshalValue([$type[0] => $v]);
 | 
        
           |  |  | 294 |                 }
 | 
        
           |  |  | 295 |                 return new SetValue($value);
 | 
        
           |  |  | 296 |         }
 | 
        
           |  |  | 297 |   | 
        
           |  |  | 298 |         throw new \UnexpectedValueException("Unexpected type: {$type}.");
 | 
        
           |  |  | 299 |     }
 | 
        
           |  |  | 300 |   | 
        
           |  |  | 301 |     /**
 | 
        
           |  |  | 302 |      * Handle invalid value based on marshaler configuration.
 | 
        
           |  |  | 303 |      *
 | 
        
           |  |  | 304 |      * @param string $message Error message
 | 
        
           |  |  | 305 |      *
 | 
        
           |  |  | 306 |      * @return array|null
 | 
        
           |  |  | 307 |      */
 | 
        
           |  |  | 308 |     private function handleInvalid($message)
 | 
        
           |  |  | 309 |     {
 | 
        
           |  |  | 310 |         if ($this->options['ignore_invalid']) {
 | 
        
           |  |  | 311 |             return null;
 | 
        
           |  |  | 312 |         }
 | 
        
           |  |  | 313 |   | 
        
           |  |  | 314 |         if ($this->options['nullify_invalid']) {
 | 
        
           |  |  | 315 |             return ['NULL' => true];
 | 
        
           |  |  | 316 |         }
 | 
        
           |  |  | 317 |   | 
        
           |  |  | 318 |         throw new \UnexpectedValueException("Marshaling error: {$message}.");
 | 
        
           |  |  | 319 |     }
 | 
        
           |  |  | 320 | }
 |