├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── src └── JsonSerializer.php └── test ├── fixtures.php └── test.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | - hhvm 10 | 11 | before_script: 12 | - 'composer install --dev --prefer-source' 13 | 14 | script: php test/test.php 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mindplay/jsonfreeze 2 | =================== 3 | 4 | Serialize and unserialize a PHP object-graph to/from a JSON string-representation. 5 | 6 | [![Build Status](https://travis-ci.org/mindplay-dk/jsonfreeze.svg)](https://travis-ci.org/mindplay-dk/jsonfreeze) 7 | 8 | 9 | ## Overview 10 | 11 | This library can serialize and unserialize a complete PHP object-graph to/from a 12 | JSON string-representation. 13 | 14 | This library differs in a number of ways from e.g. `json_encode()`, `serialize()`, 15 | `var_export()` and other existing serialization libraries, in a number of important 16 | ways. 17 | 18 | [Please see here for detailed technical background information](http://stackoverflow.com/questions/10489876). 19 | 20 | The most important thing to understand, is that this library is designed to store 21 | self-contained object-graphs - it does not support shared or circular object-references. 22 | This is by design, and in-tune with good DDD design practices. An object-graph with 23 | shared or circular references cannot be stored directly as JSON, in a predictable format, 24 | primarily because the JSON data-format is a tree, not a graph. 25 | 26 | 27 | ## Usage 28 | 29 | Nothing to it. 30 | 31 | ```php 32 | use mindplay\jsonfreeze\JsonSerializer; 33 | 34 | $serializer = new JsonSerializer(); 35 | 36 | // serialize to JSON: 37 | 38 | $string = $serializer->serialize($my_object); 39 | 40 | // rebuild your object from JSON: 41 | 42 | $object = $serializer->unserialize($string); 43 | ``` 44 | 45 | ### Custom Serialization 46 | 47 | You can define your own un/serialization functions for a specified class: 48 | 49 | ```php 50 | $serializer = new JsonSerializer(); 51 | 52 | $serializer->defineSerialization( 53 | MyType::class, 54 | function (MyType $object) { 55 | return ["foo" => $object->foo, "bar" => $object->bar]; 56 | }, 57 | function (array $data) { 58 | return new MyType($data["foo"], $data["bar"]); 59 | } 60 | ); 61 | ``` 62 | 63 | Note that this works only for concrete classes, and not for abstract classes or 64 | interfaces - serialization functions apply to precisely one class, although you 65 | can of course register the same functions to multiple classes. 66 | 67 | #### Date and Time Serialization 68 | 69 | The `DateTime` and `DateTimeImmutable` classes have pre-registered un/serialization 70 | functions supporting a custom format, in which the date/time is stored in the common 71 | ISO-8601 date/time format in the UTC timezone, along with the timezone ID - for example: 72 | 73 | { 74 | "#type": "DateTime", 75 | "datetime": "1975-07-07T00:00:00Z", 76 | "timezone": "America\/New_York" 77 | } 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mindplay/jsonfreeze", 3 | "type": "library", 4 | "description": "JSON Serialization library", 5 | "license": "LGPL-3.0+", 6 | "authors": [ 7 | { 8 | "name": "Rasmus Schultz", 9 | "email": "rasmus@mindplay.dk" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.3.0" 14 | }, 15 | "require-dev": { 16 | "mindplay/testies": "dev-master" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "mindplay\\jsonfreeze\\": "src/" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/JsonSerializer.php: -------------------------------------------------------------------------------- 1 | serialization function 72 | */ 73 | private $serializers = array(); 74 | 75 | /** 76 | * @var callable[] map where fully-qualified class-name => serialization function 77 | */ 78 | private $unserializers = array(); 79 | 80 | /** 81 | * @param bool $pretty true to enable "pretty" JSON formatting. 82 | */ 83 | public function __construct($pretty = true) 84 | { 85 | if ($pretty) { 86 | $this->indentation = ' '; 87 | $this->newline = "\n"; 88 | $this->padding = ' '; 89 | } 90 | 91 | $this->defineSerialization( 92 | 'DateTime', 93 | array($this, "_serializeDateTime"), 94 | array($this, "_unserializeDateTime") 95 | ); 96 | 97 | $this->defineSerialization( 98 | 'DateTimeImmutable', 99 | array($this, "_serializeDateTime"), 100 | array($this, "_unserializeDateTime") 101 | ); 102 | } 103 | 104 | /** 105 | * Serialize a given PHP value/array/object-graph to a JSON representation. 106 | * 107 | * @param mixed $value The value, array, or object-graph to be serialized. 108 | * 109 | * @return string JSON serialized representation 110 | */ 111 | public function serialize($value) 112 | { 113 | return $this->_serialize($value, 0); 114 | } 115 | 116 | /** 117 | * Unserialize a value/array/object-graph from a JSON string representation. 118 | * 119 | * @param string $string JSON serialized value/array/object representation 120 | * 121 | * @return mixed The unserialized value, array or object-graph. 122 | */ 123 | public function unserialize($string) 124 | { 125 | $data = json_decode($string, true); 126 | 127 | return $this->_unserialize($data); 128 | } 129 | 130 | /** 131 | * Enable (or disable) serialization of private properties. 132 | * 133 | * By default, private properties are serialized - be aware that skipping private 134 | * properties may require some careful handling of those properties in your models; 135 | * in particular, a private property initialized during __construct() will not get 136 | * initialized when you unserialize() the object. 137 | */ 138 | public function skipPrivateProperties($skip = true) 139 | { 140 | if ($this->skip_private !== $skip) { 141 | $this->skip_private = $skip; 142 | 143 | self::$_reflections = array(); 144 | } 145 | } 146 | 147 | /** 148 | * Registers a pair of custom un/serialization functions for a given class 149 | * 150 | * @param string $type fully-qualified class-name 151 | * @param callable $serialize serialization function; takes an object and returns serialized data 152 | * @param callable $unserialize unserialization function; takes serialized data and returns an object 153 | */ 154 | public function defineSerialization($type, $serialize, $unserialize) 155 | { 156 | $this->serializers[$type] = $serialize; 157 | $this->unserializers[$type] = $unserialize; 158 | } 159 | 160 | /** 161 | * Serializes an individual object/array/hash/value, returning a JSON string representation 162 | * 163 | * @param mixed $value the value to serialize 164 | * @param int $indent indentation level 165 | * 166 | * @return string JSON serialized value 167 | */ 168 | protected function _serialize($value, $indent = 0) 169 | { 170 | if (is_object($value)) { 171 | if (get_class($value) === self::STD_CLASS) { 172 | return $this->_serializeStdClass($value, $indent); 173 | } 174 | 175 | return $this->_serializeObject($value, $indent); 176 | } elseif (is_array($value)) { 177 | if (array_keys($value) === array_keys(array_values($value))) { 178 | return $this->_serializeArray($value, $indent); 179 | } else { 180 | return $this->_serializeHash($value, $indent); 181 | } 182 | } elseif (is_string($value)) { 183 | if (preg_match('//u', $value) !== 1) { 184 | throw new RuntimeException("Malformed UTF-8 characters, possibly incorrectly encoded"); 185 | } 186 | 187 | return json_encode($value); 188 | } elseif (is_scalar($value)) { 189 | return json_encode($value); 190 | } else { 191 | return 'null'; 192 | } 193 | } 194 | 195 | /** 196 | * Serializes a complete object with aggregates, returning a JSON string representation. 197 | * 198 | * @param object $object object 199 | * @param int $indent indentation level 200 | * 201 | * @return string JSON object representation 202 | */ 203 | protected function _serializeObject($object, $indent) 204 | { 205 | $type = get_class($object); 206 | 207 | if (isset($this->serializers[$type])) { 208 | return $this->_serialize(call_user_func($this->serializers[$type], $object), $indent); 209 | } 210 | 211 | $whitespace = $this->newline . str_repeat($this->indentation, $indent + 1); 212 | 213 | $string = '{' . $whitespace . '"' . self::TYPE . '":' . $this->padding . json_encode($type); 214 | 215 | foreach ($this->_getClassProperties($type) as $name => $prop) { 216 | $string .= ',' 217 | . $whitespace 218 | . json_encode($name) 219 | . ':' 220 | . $this->padding 221 | . $this->_serialize($prop->getValue($object), $indent + 1); 222 | } 223 | 224 | $string .= $this->newline . str_repeat($this->indentation, $indent) . '}'; 225 | 226 | return $string; 227 | } 228 | 229 | /** 230 | * Serializes a "strict" array (base-0 integer keys) returning a JSON string representation. 231 | * 232 | * @param array $array array 233 | * @param int $indent indentation level 234 | * 235 | * @return string JSON array representation 236 | */ 237 | protected function _serializeArray($array, $indent) 238 | { 239 | $string = '['; 240 | 241 | $last_key = count($array) - 1; 242 | 243 | foreach ($array as $key => $item) { 244 | $string .= $this->_serialize($item, $indent) . ($key === $last_key ? '' : ','); 245 | } 246 | 247 | $string .= ']'; 248 | 249 | return $string; 250 | } 251 | 252 | /** 253 | * Serializes a "wild" array (e.g. a "hash" array with mixed keys) returning a JSON string representation. 254 | * 255 | * @param array $hash hash array 256 | * @param int $indent indentation level 257 | * 258 | * @return string JSON hash representation 259 | */ 260 | protected function _serializeHash($hash, $indent) 261 | { 262 | $whitespace = $this->newline . str_repeat($this->indentation, $indent + 1); 263 | 264 | $string = '{'; 265 | 266 | $comma = ''; 267 | 268 | foreach ($hash as $key => $item) { 269 | $string .= $comma 270 | . $whitespace 271 | . json_encode((string) $key) 272 | . ':' 273 | . $this->padding 274 | . $this->_serialize($item, $indent + 1); 275 | 276 | $comma = ','; 277 | } 278 | 279 | $string .= $this->newline . str_repeat($this->indentation, $indent) . '}'; 280 | 281 | return $string; 282 | } 283 | 284 | /** 285 | * Serializes a stdClass object returning a JSON string representation. 286 | * 287 | * @param stdClass $value stdClass object 288 | * @param int $indent indentation level 289 | * 290 | * @return string JSON object representation 291 | */ 292 | protected function _serializeStdClass($value, $indent) 293 | { 294 | $array = (array) $value; 295 | 296 | $array[self::TYPE] = self::STD_CLASS; 297 | 298 | return $this->_serializeHash($array, $indent); 299 | } 300 | 301 | /** 302 | * Unserialize an individual object/array/hash/value from a hash of properties. 303 | * 304 | * @param array $data hashed value representation 305 | * 306 | * @return mixed unserialized value 307 | */ 308 | protected function _unserialize($data) 309 | { 310 | if (! is_array($data)) { 311 | return $data; // scalar value is fully unserialized 312 | } 313 | 314 | if (array_key_exists(self::TYPE, $data)) { 315 | if ($data[self::TYPE] === self::HASH) { 316 | // remove legacy hash tag from JSON serialized with version 1.x 317 | unset($data[self::TYPE]); 318 | return $this->_unserializeArray($data); 319 | } 320 | 321 | return $this->_unserializeObject($data); 322 | } 323 | 324 | return $this->_unserializeArray($data); 325 | } 326 | 327 | /** 328 | * Unserialize an individual object from a hash of properties. 329 | * 330 | * @param array $data hash of object properties 331 | * 332 | * @return object unserialized object 333 | */ 334 | protected function _unserializeObject($data) 335 | { 336 | $type = $data[self::TYPE]; 337 | 338 | if (isset($this->unserializers[$type])) { 339 | return $this->_unserialize(call_user_func($this->unserializers[$type], $data)); 340 | } 341 | 342 | if ($type === self::STD_CLASS) { 343 | unset($data[self::TYPE]); 344 | return (object) $this->_unserializeArray($data); 345 | } 346 | 347 | $object = unserialize('O:' . strlen($type) . ':"' . $type . '":0:{}'); 348 | 349 | // TODO support ReflectionClass::newInstanceWithoutConstructor() in PHP 5.4 350 | 351 | foreach ($this->_getClassProperties($type) as $name => $prop) { 352 | if (array_key_exists($name, $data)) { 353 | $value = $this->_unserialize($data[$name]); 354 | $prop->setValue($object, $value); 355 | } 356 | } 357 | 358 | return $object; 359 | } 360 | 361 | /** 362 | * Unserialize a hash/array. 363 | * 364 | * @param array $data hash/array 365 | * 366 | * @return array unserialized hash/array 367 | */ 368 | protected function _unserializeArray($data) 369 | { 370 | $array = array(); 371 | 372 | foreach ($data as $key => $value) { 373 | $array[$key] = $this->_unserialize($value); 374 | } 375 | 376 | return $array; 377 | } 378 | 379 | /** 380 | * Obtain a (cached) array of property-reflections, with all properties made accessible. 381 | * 382 | * @param string $type fully-qualified class name 383 | * 384 | * @return ReflectionProperty[] map where property-name => accessible ReflectionProperty instance 385 | */ 386 | protected function _getClassProperties($type) 387 | { 388 | if (! isset(self::$_reflections[$type])) { 389 | $class = new ReflectionClass($type); 390 | 391 | $props = array(); 392 | 393 | do { 394 | foreach ($class->getProperties() as $prop) { 395 | if ($prop->isStatic()) { 396 | continue; // omit static property 397 | } 398 | 399 | if ($this->skip_private && $prop->isPrivate()) { 400 | continue; // skip private property 401 | } 402 | 403 | $prop->setAccessible(true); 404 | 405 | $name = ($prop->isPrivate() && $prop->class !== $type) 406 | ? "{$prop->class}#" . $prop->getName() 407 | : $prop->getName(); 408 | 409 | $props[$name] = $prop; 410 | } 411 | } while ($class = $class->getParentClass()); 412 | 413 | self::$_reflections[$type] = $props; 414 | } 415 | 416 | return self::$_reflections[$type]; 417 | } 418 | 419 | /** 420 | * @param DateTime|DateTimeImmutable $datetime 421 | * 422 | * @return array 423 | */ 424 | protected function _serializeDateTime($datetime) 425 | { 426 | $utc = date_create_from_format("U", $datetime->format("U"), timezone_open("UTC")); 427 | 428 | return array( 429 | self::TYPE => get_class($datetime), 430 | "datetime" => $utc->format(self::DATETIME_FORMAT), 431 | "timezone" => $datetime->getTimezone()->getName(), 432 | ); 433 | } 434 | 435 | /** 436 | * @param array $data 437 | * 438 | * @return DateTime|DateTimeImmutable 439 | */ 440 | protected function _unserializeDateTime($data) 441 | { 442 | switch ($data[self::TYPE]) { 443 | case "DateTime": 444 | $datetime = DateTime::createFromFormat(self::DATETIME_FORMAT, $data["datetime"], timezone_open("UTC")); 445 | 446 | $datetime->setTimezone(timezone_open($data["timezone"])); 447 | 448 | return $datetime; 449 | 450 | case "DateTimeImmutable": 451 | $datetime = DateTimeImmutable::createFromFormat(self::DATETIME_FORMAT, $data["datetime"], timezone_open("UTC")); 452 | 453 | return $datetime->setTimezone(timezone_open($data["timezone"])); 454 | 455 | default: 456 | throw new RuntimeException("unsupported type: " . $data[self::TYPE]); 457 | } 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /test/fixtures.php: -------------------------------------------------------------------------------- 1 | orderNo = 123; 10 | $order->setPaid(); 11 | $order->addLine(new OrderLine('milk "fuzz"', 3)); 12 | 13 | $cookies = new OrderLine('cookies', 7); 14 | $cookies->options = array('flavor' => 'chocolate', 'weight' => '1/2 lb'); 15 | 16 | $order->addLine($cookies); 17 | 18 | return $order; 19 | } 20 | 21 | class Order 22 | { 23 | /** 24 | * @var int 25 | */ 26 | public $orderNo; 27 | 28 | /** 29 | * @var OrderLine[] 30 | */ 31 | public $lines = array(); 32 | 33 | /** 34 | * @var bool 35 | */ 36 | public $paid = false; 37 | 38 | public function addLine(OrderLine $line) 39 | { 40 | $this->lines[] = $line; 41 | } 42 | 43 | public function setPaid($paid = true) 44 | { 45 | $this->paid = true; 46 | } 47 | } 48 | 49 | class OrderLine 50 | { 51 | /** 52 | * @param string $item 53 | * @param int $amount 54 | */ 55 | public function __construct($item, $amount) 56 | { 57 | $this->item = $item; 58 | $this->amount = $amount; 59 | $this->data = 456; 60 | } 61 | 62 | /** 63 | * @var array this is here to assert omission of static properties 64 | */ 65 | public static $cache = array(); 66 | 67 | /** 68 | * @var int this is here to assert omission/inclusion/inheritance of private properties 69 | */ 70 | private $data = 123; 71 | 72 | public $item; 73 | public $amount; 74 | public $options = array(); 75 | 76 | public function setData($data) 77 | { 78 | $this->data = $data; 79 | } 80 | 81 | public function getData() 82 | { 83 | return $this->data; 84 | } 85 | } 86 | 87 | class OrderLineEx extends OrderLine 88 | { 89 | public $color; 90 | 91 | private $data; 92 | 93 | public function setDataEx($data) 94 | { 95 | $this->data = $data; 96 | } 97 | 98 | public function getDataEx() 99 | { 100 | return $this->data; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/test.php: -------------------------------------------------------------------------------- 1 | color = "blue"; 14 | $input->setData("one"); 15 | $input->setDataEx("two"); 16 | 17 | $serializer = new JsonSerializer(false); 18 | 19 | /** @var OrderLineEx $output */ 20 | 21 | $json = $serializer->serialize($input); 22 | 23 | eq($json, '{"#type":"OrderLineEx","color":"blue","data":"two","item":"shoe","amount":2,"options":[],"OrderLine#data":"one"}'); 24 | // ^^^^^^^^^^^^ own private ^^^^^^^^^^^^^^^^^^^^^^ inherited private 25 | $output = $serializer->unserialize($json); 26 | 27 | eq($output->item, "shoe"); 28 | eq($output->color, "blue"); 29 | eq($output->amount, 2); 30 | eq($output->getData(), "one"); 31 | eq($output->getDataEx(), "two"); 32 | } 33 | ); 34 | 35 | exit(run()); 36 | 37 | test( 38 | 'Can serialize and unserialize object graph', 39 | function () { 40 | $input = get_order_fixture(); 41 | 42 | $json = new JsonSerializer(); 43 | 44 | $data = $json->serialize($input); 45 | 46 | /** @var Order $output */ 47 | $output = $json->unserialize($data); 48 | 49 | eq($output->orderNo, $input->orderNo, 'object property value'); 50 | eq(array_keys($output->lines), array_keys($input->lines), 'array sequence'); 51 | eq($output->paid, $input->paid, 'scalar value'); 52 | 53 | eq($output->lines[0]->amount, $input->lines[0]->amount, 'nested value'); 54 | eq($output->lines[1]->options, $input->lines[1]->options, 'nested array'); 55 | 56 | $serial = json_decode($data, true); 57 | 58 | ok(! isset($serial['lines'][0]['cache']), 'static properties should always be omitted'); 59 | 60 | eq($serial['lines'][0]['data'], 456, 'private properties should be included by default'); 61 | 62 | $prop = new ReflectionProperty($output->lines[0], 'data'); 63 | $prop->setAccessible(true); 64 | 65 | $json->skipPrivateProperties(); 66 | 67 | $data = $json->serialize($input); 68 | 69 | $output = $json->unserialize($data); 70 | 71 | $serial = json_decode($data, true); 72 | 73 | ok(! isset($serial['lines'][0]['data']), 'private properties should be omitted after skipPrivateProperties()'); 74 | 75 | eq($prop->getValue($output->lines[0]), 123, 'private properties should initialize to their default value'); 76 | } 77 | ); 78 | 79 | test( 80 | 'Can serialize/unserialize standard objects', 81 | function () { 82 | $serializer = new JsonSerializer(); 83 | 84 | $input = (object) array('foo' => 'bar'); 85 | 86 | $json = $serializer->serialize($input); 87 | 88 | $data = json_decode($json, true); 89 | 90 | eq($data[JsonSerializer::TYPE], JsonSerializer::STD_CLASS, 'stdClass object gets tagged'); 91 | 92 | ok(isset($data['foo']), 'undefined property is preserved'); 93 | 94 | eq($data['foo'], 'bar', 'undefined property value is preserved'); 95 | 96 | $output = $serializer->unserialize($json); 97 | 98 | ok(isset($output->foo), 'object property is restored'); 99 | 100 | eq($output->foo, $input->foo, 'property value is restored'); 101 | } 102 | ); 103 | 104 | test( 105 | 'Can unserialize legacy array/hash values', 106 | function () { 107 | $array = array('foo', 'bar', 'baz'); 108 | 109 | $input = array( 110 | 'array' => $array, 111 | 'hash' => array( 112 | JsonSerializer::TYPE => JsonSerializer::HASH, 113 | 'foo' => 1, 114 | 'bar' => 2, 115 | 'baz' => 3, 116 | ), 117 | ); 118 | 119 | $serializer = new JsonSerializer(); 120 | 121 | $output = $serializer->unserialize(json_encode($input)); 122 | 123 | eq($output['array'], $array, 'correctly unserializes a straight array'); 124 | 125 | ok(! isset($output['hash'][JsonSerializer::TYPE]), 'legacy hash tag detected and removed'); 126 | 127 | eq( 128 | $output['hash'], 129 | array( 130 | 'foo' => 1, 131 | 'bar' => 2, 132 | 'baz' => 3, 133 | ), 134 | 'original hash correctly restored after hash tag removal' 135 | ); 136 | } 137 | ); 138 | 139 | test( 140 | 'can un/serialize arrays with mixed key-types', 141 | function () { 142 | $input = array('a', 'b' => 'b '); 143 | 144 | $serializer = new JsonSerializer(); 145 | 146 | $json = $serializer->serialize($input); 147 | 148 | $unserialized = $serializer->unserialize($json); 149 | 150 | eq($unserialized, $input); 151 | 152 | $keys = array_keys($unserialized); 153 | 154 | eq(gettype($keys[0]), 'integer'); 155 | 156 | eq(gettype($keys[1]), 'string'); 157 | } 158 | ); 159 | 160 | /** 161 | * @param DateTime|DateTimeImmutable $value 162 | * @param DateTime|DateTimeImmutable $expected 163 | */ 164 | function eq_dates($value, $expected) { 165 | eq($value->getTimestamp(), $expected->getTimestamp()); 166 | eq($value->getTimezone()->getName(), $expected->getTimezone()->getName()); 167 | } 168 | 169 | /** 170 | * @param DateTime|DateTimeImmutable $date 171 | */ 172 | function check_date($date) 173 | { 174 | $serializer = new JsonSerializer(false); 175 | 176 | $serialized = $serializer->serialize($date); 177 | 178 | eq_dates($date, $serializer->unserialize($serialized)); 179 | } 180 | 181 | test( 182 | 'Can serialize/unserialize DateTime types', 183 | function () { 184 | $serializer = new JsonSerializer(false); 185 | 186 | $date = new DateTime("1975-07-07 00:00:00", timezone_open("UTC")); 187 | 188 | eq($serializer->serialize($date), '{"#type":"DateTime","datetime":"1975-07-07T00:00:00Z","timezone":"UTC"}'); 189 | 190 | check_date($date); 191 | 192 | $date = new DateTime("1975-07-07 00:00:00", timezone_open("America/New_York")); 193 | 194 | eq($serializer->serialize($date), '{"#type":"DateTime","datetime":"1975-07-07T04:00:00Z","timezone":"America\/New_York"}'); 195 | 196 | check_date($date); 197 | 198 | if (class_exists("DateTimeImmutable")) { 199 | $date = new DateTimeImmutable("1975-07-07 00:00:00", timezone_open("UTC")); 200 | 201 | eq($serializer->serialize($date), '{"#type":"DateTimeImmutable","datetime":"1975-07-07T00:00:00Z","timezone":"UTC"}'); 202 | 203 | check_date($date); 204 | 205 | $date = new DateTimeImmutable("1975-07-07 00:00:00", timezone_open("America/New_York")); 206 | 207 | eq($serializer->serialize($date), '{"#type":"DateTimeImmutable","datetime":"1975-07-07T04:00:00Z","timezone":"America\/New_York"}'); 208 | 209 | check_date($date); 210 | } 211 | } 212 | ); 213 | 214 | test( 215 | 'Throws for invalid UTF-8 strings', 216 | function () { 217 | $serializer = new JsonSerializer(false); 218 | 219 | $invalid_string = "\xc3\x28"; // invalid 2 Octet Sequence 220 | 221 | expect( 222 | 'RuntimeException', 223 | 'should throw for invalid UTF-8 strings', 224 | function () use ($serializer, $invalid_string) { 225 | $serializer->serialize($invalid_string); 226 | }, 227 | '#' . preg_quote('Malformed UTF-8 characters, possibly incorrectly encoded') . '#' 228 | ); 229 | } 230 | ); 231 | 232 | exit(run()); 233 | --------------------------------------------------------------------------------