From 93347a53914b5d737f9bd5a15848953c014de6cb Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Sun, 16 Dec 2018 17:37:21 +0100 Subject: [PATCH] Fix additionalProperties check (#64) --- composer.json | 1 + phpstan.neon | 2 + src/Constraint/Properties.php | 84 +++++++-- src/Schema.php | 168 +++++++++--------- src/Structure/ClassStructureTrait.php | 8 +- src/Wrapper.php | 2 +- tests/src/Helper/Order.php | 6 +- tests/src/Helper/User63.php | 142 +++++++++++++++ .../PHPUnit/ClassStructure/MappingTest.php | 71 ++++++++ tests/src/PHPUnit/Example/ExampleTest.php | 12 +- tests/src/PHPUnit/Suite/Issue63Test.php | 36 ++++ 11 files changed, 428 insertions(+), 104 deletions(-) create mode 100644 tests/src/Helper/User63.php create mode 100644 tests/src/PHPUnit/ClassStructure/MappingTest.php create mode 100644 tests/src/PHPUnit/Suite/Issue63Test.php diff --git a/composer.json b/composer.json index 7d183d6..397d31b 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "type": "library", "require": { "php": ">=5.4", + "ext-json": "*", "phplang/scope-exit": "^1.0", "swaggest/json-diff": "^3.4.2" }, diff --git a/phpstan.neon b/phpstan.neon index a23ca3f..d30fac7 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,3 +3,5 @@ parameters: - '#PHPDoc tag @param references unknown parameter \$schema#' - '#Access to an undefined property static\(Swaggest\\JsonSchema\\JsonSchema\)\|Swaggest\\JsonSchema\\Constraint\\Properties::#' - '#Accessing property \$skipValidation on possibly null value of type Swaggest\\JsonSchema\\Context\|null#' + - '#Accessing property \$__propertyToData on possibly null value of type Swaggest\\JsonSchema\\Constraint\\Properties\|null#' + - '#Accessing property \$__dataToProperty on possibly null value of type Swaggest\\JsonSchema\\Constraint\\Properties\|null#' diff --git a/src/Constraint/Properties.php b/src/Constraint/Properties.php index f51d292..a99db73 100644 --- a/src/Constraint/Properties.php +++ b/src/Constraint/Properties.php @@ -11,7 +11,6 @@ /** * @method SchemaContract __get($key) - * @method Schema[] toArray() */ class Properties extends ObjectItem implements Constraint { @@ -23,11 +22,70 @@ class Properties extends ObjectItem implements Constraint /** @var Schema */ protected $__schema; + /** + * @var Schema[] + */ + private $__mappedProperties; + + /** + * @var array + */ + private $__dataKeyMaps = array(); + + /** + * Data to property mapping, example ["$ref" => "ref"] + * @var array + */ + public $__dataToProperty = array(); + /** * Property to data mapping, example ["ref" => "$ref"] * @var array */ - public $__defaultMapping = array(); + public $__propertyToData = array(); + + /** + * Returns a map of properties by default data name + * @return Schema[] + */ + public function &toArray() + { + if (!isset($this->__propertyToData[Schema::DEFAULT_MAPPING])) { + return $this->__arrayOfData; + } + if (null === $this->__mappedProperties) { + $properties = array(); + foreach ($this->__arrayOfData as $propertyName => $property) { + if (isset($this->__propertyToData[Schema::DEFAULT_MAPPING][$propertyName])) { + $propertyName = $this->__propertyToData[Schema::DEFAULT_MAPPING][$propertyName]; + } + $properties[$propertyName] = $property; + } + $this->__mappedProperties = $properties; + } + return $this->__mappedProperties; + } + + /** + * @param string $mapping + * @return string[] a map of propertyName to dataName + */ + public function getDataKeyMap($mapping = Schema::DEFAULT_MAPPING) + { + if (!isset($this->__dataKeyMaps[$mapping])) { + $map = array(); + foreach ($this->__arrayOfData as $propertyName => $property) { + if (isset($this->__propertyToData[$mapping][$propertyName])) { + $map[$propertyName] = $this->__propertyToData[$mapping][$propertyName]; + } else { + $map[$propertyName] = $propertyName; + } + } + $this->__dataKeyMaps[$mapping] = $map; + } + + return $this->__dataKeyMaps[$mapping]; + } public function lock() { @@ -35,6 +93,13 @@ public function lock() return $this; } + public function addPropertyMapping($dataName, $propertyName, $mapping = Schema::DEFAULT_MAPPING) + { + $this->__dataToProperty[$mapping][$dataName] = $propertyName; + $this->__propertyToData[$mapping][$propertyName] = $dataName; + } + + /** * @param string $name * @param mixed $column @@ -101,7 +166,8 @@ public function isEmpty() public function jsonSerialize() { - $result = $this->__arrayOfData; + $result = $this->toArray(); + if ($this->__nestedObjects) { foreach ($this->__nestedObjects as $object) { foreach ($object->toArray() as $key => $value) { @@ -110,18 +176,6 @@ public function jsonSerialize() } } - if (isset($this->__defaultMapping)) { - $mappedResult = new \stdClass(); - foreach ($result as $key => $value) { - if (isset($this->__defaultMapping[$key])) { - $mappedResult->{$this->__defaultMapping[$key]} = $value; - } else { - $mappedResult->$key = $value; - } - } - return $mappedResult; - } - return (object)$result; } diff --git a/src/Schema.php b/src/Schema.php index 5e5309d..5121318 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -47,7 +47,7 @@ class Schema extends JsonSchema implements MetaHolder, SchemaContract const PROP_ID_D4 = 'id'; // Object - /** @var null|Properties|Schema[]|Schema */ + /** @var null|Properties */ public $properties; /** @var Schema|bool */ public $additionalProperties; @@ -80,21 +80,22 @@ class Schema extends JsonSchema implements MetaHolder, SchemaContract public $objectItemClass; - private $useObjectAsArray = false; - private $__dataToProperty = array(); - private $__propertyToData = array(); + /** + * @todo check usages/deprecate + * @var bool + */ + private $useObjectAsArray = false; private $__booleanSchema; public function addPropertyMapping($dataName, $propertyName, $mapping = self::DEFAULT_MAPPING) { - $this->__dataToProperty[$mapping][$dataName] = $propertyName; - $this->__propertyToData[$mapping][$propertyName] = $dataName; - - if ($mapping === self::DEFAULT_MAPPING && $this->properties instanceof Properties) { - $this->properties->__defaultMapping[$propertyName] = $dataName; + if (null === $this->properties) { + $this->properties = new Properties(); } + + $this->properties->addPropertyMapping($dataName, $propertyName, $mapping); return $this; } @@ -530,39 +531,16 @@ private function processIf($data, Context $options, $path) } /** - * @param object $data + * @param array $array * @param Context $options * @param string $path * @throws InvalidValue */ - private function processObjectRequired($data, Context $options, $path) + private function processObjectRequired($array, Context $options, $path) { - if (isset($this->__dataToProperty[$options->mapping])) { - if ($options->import) { - foreach ($this->required as $item) { - if (isset($this->__propertyToData[$options->mapping][$item])) { - $item = $this->__propertyToData[$options->mapping][$item]; - } - if (!property_exists($data, $item)) { - $this->fail(new ObjectException('Required property missing: ' . $item . ', data: ' . json_encode($data, JSON_UNESCAPED_SLASHES), ObjectException::REQUIRED), $path); - } - } - } else { - foreach ($this->required as $item) { - if (isset($this->__dataToProperty[$options->mapping][$item])) { - $item = $this->__dataToProperty[$options->mapping][$item]; - } - if (!property_exists($data, $item)) { - $this->fail(new ObjectException('Required property missing: ' . $item . ', data: ' . json_encode($data, JSON_UNESCAPED_SLASHES), ObjectException::REQUIRED), $path); - } - } - } - - } else { - foreach ($this->required as $item) { - if (!property_exists($data, $item)) { - $this->fail(new ObjectException('Required property missing: ' . $item . ', data: ' . json_encode($data, JSON_UNESCAPED_SLASHES), ObjectException::REQUIRED), $path); - } + foreach ($this->required as $item) { + if (!array_key_exists($item, $array)) { + $this->fail(new ObjectException('Required property missing: ' . $item . ', data: ' . json_encode($array, JSON_UNESCAPED_SLASHES), ObjectException::REQUIRED), $path); } } } @@ -581,10 +559,34 @@ private function processObject($data, Context $options, $path, $result = null) { $import = $options->import; + $hasMapping = isset($this->properties->__dataToProperty[$options->mapping]); + + $array = !$data instanceof \stdClass ? get_object_vars($data) : (array)$data; + + // convert imported data to default mapping before validation + if ($import && $options->mapping !== self::DEFAULT_MAPPING) { + if (isset($this->properties->__dataToProperty[$options->mapping])) { + foreach ($this->properties->__dataToProperty[$options->mapping] as $dataName => $propertyName) { + if (!isset($array[$dataName])) { + continue; + } + + $propertyName = isset($this->properties->__propertyToData[self::DEFAULT_MAPPING][$propertyName]) + ? $this->properties->__propertyToData[self::DEFAULT_MAPPING][$propertyName] + : $propertyName; + if ($propertyName !== $dataName) { + $array[$propertyName] = $array[$dataName]; + unset($array[$dataName]); + } + } + } + } + if (!$options->skipValidation && $this->required !== null) { - $this->processObjectRequired($data, $options, $path); + $this->processObjectRequired($array, $options, $path); } + // build result entity if ($import) { if (!$options->validateOnly) { @@ -630,10 +632,10 @@ private function processObject($data, Context $options, $path, $result = null) // @todo better check for schema id if ($import - && isset($data->{Schema::PROP_ID_D4}) + && isset($array[Schema::PROP_ID_D4]) && ($options->version === Schema::VERSION_DRAFT_04 || $options->version === Schema::VERSION_AUTO) - && is_string($data->{Schema::PROP_ID_D4})) { - $id = $data->{Schema::PROP_ID_D4}; + && is_string($array[Schema::PROP_ID_D4])) { + $id = $array[Schema::PROP_ID_D4]; $refResolver = $options->refResolver; $parentScope = $refResolver->updateResolutionScope($id); /** @noinspection PhpUnusedLocalVariableInspection */ @@ -643,10 +645,10 @@ private function processObject($data, Context $options, $path, $result = null) } if ($import - && isset($data->{self::PROP_ID}) + && isset($array[self::PROP_ID]) && ($options->version >= Schema::VERSION_DRAFT_06 || $options->version === Schema::VERSION_AUTO) - && is_string($data->{self::PROP_ID})) { - $id = $data->{self::PROP_ID}; + && is_string($array[self::PROP_ID])) { + $id = $array[self::PROP_ID]; $refResolver = $options->refResolver; $parentScope = $refResolver->updateResolutionScope($id); /** @noinspection PhpUnusedLocalVariableInspection */ @@ -655,18 +657,15 @@ private function processObject($data, Context $options, $path, $result = null) }); } + // check $ref if ($import) { try { $refProperty = null; $dereference = true; - if (isset($data->{self::PROP_REF})) { - if (null === $refProperty = $this->properties[self::PROP_REF]) { - if (isset($this->__dataToProperty[$options->mapping][self::PROP_REF])) { - $refProperty = $this->properties[$this->__dataToProperty[$options->mapping][self::PROP_REF]]; - } - } + if (isset($array[self::PROP_REF])) { + $refProperty = $this->properties[self::PROP_REF]; if (isset($refProperty) && ($refProperty->format !== Format::URI_REFERENCE)) { $dereference = false; @@ -674,11 +673,11 @@ private function processObject($data, Context $options, $path, $result = null) } if ( - isset($data->{self::PROP_REF}) - && is_string($data->{self::PROP_REF}) + isset($array[self::PROP_REF]) + && is_string($array[self::PROP_REF]) && $dereference ) { - $refString = $data->{self::PROP_REF}; + $refString = $array[self::PROP_REF]; // todo check performance impact if ($refString === 'http://json-schema.org/draft-04/schema#' @@ -732,6 +731,7 @@ private function processObject($data, Context $options, $path, $result = null) $nestedProperties = null; if ($this->properties !== null) { $properties = $this->properties->toArray(); // todo call directly + if ($this->properties instanceof Properties) { $nestedProperties = $this->properties->nestedProperties; } else { @@ -739,23 +739,6 @@ private function processObject($data, Context $options, $path, $result = null) } } - $array = array(); - if (!empty($this->__dataToProperty[$options->mapping])) { // todo skip on $options->validateOnly - foreach (!$data instanceof \stdClass ? get_object_vars($data) : (array)$data as $key => $value) { - if ($import) { - if (isset($this->__dataToProperty[$options->mapping][$key])) { - $key = $this->__dataToProperty[$options->mapping][$key]; - } - } else { - if (isset($this->__propertyToData[$options->mapping][$key])) { - $key = $this->__propertyToData[$options->mapping][$key]; - } - } - $array[$key] = $value; - } - } else { - $array = !$data instanceof \stdClass ? get_object_vars($data) : (array)$data; - } if (!$options->skipValidation) { if ($this->minProperties !== null && count($array) < $this->minProperties) { @@ -781,9 +764,6 @@ private function processObject($data, Context $options, $path, $result = null) foreach ($properties as $key => $property) { // todo check when property is \stdClass `{}` here (RefTest) if ($property instanceof SchemaContract && null !== $default = $property->getDefault()) { - if (isset($this->__dataToProperty[$options->mapping][$key])) { - $key = $this->__dataToProperty[$options->mapping][$key]; - } if (!array_key_exists($key, $array)) { $defaultApplied[$key] = true; $array[$key] = $default; @@ -808,7 +788,7 @@ private function processObject($data, Context $options, $path, $result = null) $dependencies->process($data, $options, $path . '->dependencies:' . $key); } else { foreach ($dependencies as $item) { - if (!property_exists($data, $item)) { + if (!array_key_exists($item, $array)) { $this->fail(new ObjectException('Dependency property missing: ' . $item, ObjectException::DEPENDENCY_MISSING), $path); } @@ -868,21 +848,45 @@ private function processObject($data, Context $options, $path, $result = null) } } + $propertyName = $key; + + if ($hasMapping) { + if (isset($this->properties->__dataToProperty[self::DEFAULT_MAPPING][$key])) { + // todo check performance of local map access + $propertyName = $this->properties->__dataToProperty[self::DEFAULT_MAPPING][$key]; + } + } + + if ($options->mapping !== self::DEFAULT_MAPPING) { + if (!$import) { + if (isset($this->properties->__propertyToData[$options->mapping][$propertyName])) { + // todo check performance of local map access + $propertyName = $this->properties->__propertyToData[$options->mapping][$propertyName]; + } + } + } + if (!$options->validateOnly && $nestedEggs && $import) { foreach ($nestedEggs as $nestedEgg) { $result->setNestedProperty($key, $value, $nestedEgg); } if ($propertyFound) { - $result->$key = $value; + $result->$propertyName = $value; } } else { + if (!$import && $hasMapping) { + if (isset($this->properties->__propertyToData[$options->mapping][$propertyName])) { + $propertyName = $this->properties->__propertyToData[$options->mapping][$propertyName]; + } + } + if ($this->useObjectAsArray && $import) { - $result[$key] = $value; + $result[$propertyName] = $value; } else { if ($found || !$import) { - $result->$key = $value; - } elseif (!isset($result->$key)) { - $result->$key = $value; + $result->$propertyName = $value; + } elseif (!isset($result->$propertyName)) { + $result->$propertyName = $value; } } } @@ -1016,6 +1020,7 @@ private function processContent($data, Context $options, $path) */ public function process($data, Context $options, $path = '#', $result = null) { + $origData = $data; $import = $options->import; if (!$import && $data instanceof SchemaExporter) { @@ -1050,7 +1055,8 @@ public function process($data, Context $options, $path = '#', $result = null) unset($exported); } - for ($i = 1; $i < count($refs); $i++) { + $countRefs = count($refs); + for ($i = 1; $i < $countRefs; $i++) { $ref = $refs[$i]; if (!array_key_exists($ref, $options->exportedDefinitions) && strpos($ref, '://') === false) { $exported = new \stdClass(); @@ -1059,7 +1065,7 @@ public function process($data, Context $options, $path = '#', $result = null) } } - $result->{self::PROP_REF} = $refs[count($refs) - 1]; + $result->{self::PROP_REF} = $refs[$countRefs - 1]; return $result; } } diff --git a/src/Structure/ClassStructureTrait.php b/src/Structure/ClassStructureTrait.php index ac5b86d..ea58d85 100644 --- a/src/Structure/ClassStructureTrait.php +++ b/src/Structure/ClassStructureTrait.php @@ -96,10 +96,10 @@ public function jsonSerialize() { $result = new \stdClass(); $schema = static::schema(); - foreach ($schema->getPropertyNames() as $name) { - $value = $this->$name; - if ((null !== $value) || array_key_exists($name, $this->__arrayOfData)) { - $result->$name = $value; + foreach ($schema->getProperties()->getDataKeyMap() as $propertyName => $dataName) { + $value = $this->$propertyName; + if ((null !== $value) || array_key_exists($propertyName, $this->__arrayOfData)) { + $result->$dataName = $value; } } foreach ($schema->getNestedPropertyNames() as $name) { diff --git a/src/Wrapper.php b/src/Wrapper.php index fdca39f..0a0a24a 100644 --- a/src/Wrapper.php +++ b/src/Wrapper.php @@ -92,7 +92,7 @@ public function nested() } /** - * @return null|Constraint\Properties|Schema|Schema[] + * @return null|Constraint\Properties */ public function getProperties() { diff --git a/tests/src/Helper/Order.php b/tests/src/Helper/Order.php index 743b062..72b6737 100644 --- a/tests/src/Helper/Order.php +++ b/tests/src/Helper/Order.php @@ -37,7 +37,11 @@ public static function setUpProperties($properties, Schema $ownerSchema) $properties->dateTime->format = Format::DATE_TIME; $properties->price = Schema::number(); - $ownerSchema->required[] = self::names()->id; + $ownerSchema->required = array( + self::names()->id, + 'date_time', + self::names()->price + ); $ownerSchema->setFromRef('#/definitions/order'); // Define default mapping if any diff --git a/tests/src/Helper/User63.php b/tests/src/Helper/User63.php new file mode 100644 index 0000000..83ccea2 --- /dev/null +++ b/tests/src/Helper/User63.php @@ -0,0 +1,142 @@ +id = Schema::integer(); + $properties->id->description = "The person's ID."; + $properties->firstName = Schema::string(); + $properties->firstName->description = "The person's first name."; + $ownerSchema->addPropertyMapping('first_name', self::names()->firstName); + $properties->lastName = Schema::string(); + $properties->lastName->description = "The person's last name."; + $ownerSchema->addPropertyMapping('last_name', self::names()->lastName); + $properties->age = Schema::integer(); + $properties->age->description = "Age in years which must be equal to or greater than zero."; + $properties->age->minimum = 0; + $ownerSchema->type = 'object'; + $ownerSchema->additionalProperties = false; + $ownerSchema->schema = "http://json-schema.org/draft-07/schema#"; + $ownerSchema->title = "User"; + $ownerSchema->required = array( + 0 => 'id', + 1 => 'first_name', + 2 => 'last_name', + 3 => 'age', + ); + } + + /** + * @return int + * @codeCoverageIgnoreStart + */ + public function getId() + { + return $this->id; + } + /** @codeCoverageIgnoreEnd */ + + /** + * @param int $id The person's ID. + * @return $this + * @codeCoverageIgnoreStart + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + /** @codeCoverageIgnoreEnd */ + + /** + * @return string + * @codeCoverageIgnoreStart + */ + public function getFirstName() + { + return $this->firstName; + } + /** @codeCoverageIgnoreEnd */ + + /** + * @param string $firstName The person's first name. + * @return $this + * @codeCoverageIgnoreStart + */ + public function setFirstName($firstName) + { + $this->firstName = $firstName; + return $this; + } + /** @codeCoverageIgnoreEnd */ + + /** + * @return string + * @codeCoverageIgnoreStart + */ + public function getLastName() + { + return $this->lastName; + } + /** @codeCoverageIgnoreEnd */ + + /** + * @param string $lastName The person's last name. + * @return $this + * @codeCoverageIgnoreStart + */ + public function setLastName($lastName) + { + $this->lastName = $lastName; + return $this; + } + /** @codeCoverageIgnoreEnd */ + + /** + * @return int + * @codeCoverageIgnoreStart + */ + public function getAge() + { + return $this->age; + } + /** @codeCoverageIgnoreEnd */ + + /** + * @param int $age Age in years which must be equal to or greater than zero. + * @return $this + * @codeCoverageIgnoreStart + */ + public function setAge($age) + { + $this->age = $age; + return $this; + } + /** @codeCoverageIgnoreEnd */ +} \ No newline at end of file diff --git a/tests/src/PHPUnit/ClassStructure/MappingTest.php b/tests/src/PHPUnit/ClassStructure/MappingTest.php new file mode 100644 index 0000000..acf20e7 --- /dev/null +++ b/tests/src/PHPUnit/ClassStructure/MappingTest.php @@ -0,0 +1,71 @@ +id = 1; + $order->dateTime = '2015-10-28T07:28:00Z'; + $order->price = 2.2; + /** @noinspection PhpUnhandledExceptionInspection */ + $exported = Order::export($order); + $json = <<assertSame($json, json_encode($exported, JSON_PRETTY_PRINT)); + + + /** @noinspection PhpUnhandledExceptionInspection */ + $imported = Order::import(json_decode($json)); + $this->assertSame(1, $imported->id); + $this->assertSame('2015-10-28T07:28:00Z', $imported->dateTime); + $this->assertSame(2.2, $imported->price); + + } + + /** + * @throws \Swaggest\JsonSchema\Exception + * @throws \Swaggest\JsonSchema\InvalidValue + */ + public function testNameMapperNonDefault() + { + $order = new Order(); + $order->id = 1; + $order->dateTime = '2015-10-28T07:28:00Z'; + $order->price = 2.2; + + $options = new Context(); + $options->mapping = Order::FANCY_MAPPING; + + Order::schema(); + + /** @noinspection PhpUnhandledExceptionInspection */ + $exported = Order::export($order, $options); + $json = <<assertSame($json, json_encode($exported, JSON_PRETTY_PRINT)); + + $imported = Order::import(json_decode($json), $options); + $this->assertSame('2015-10-28T07:28:00Z', $imported->dateTime); + + } + + +} \ No newline at end of file diff --git a/tests/src/PHPUnit/Example/ExampleTest.php b/tests/src/PHPUnit/Example/ExampleTest.php index 1aa6670..5fb6e52 100644 --- a/tests/src/PHPUnit/Example/ExampleTest.php +++ b/tests/src/PHPUnit/Example/ExampleTest.php @@ -122,7 +122,7 @@ public function testExample() $order->dateTime = '2015-10-28T07:28:00Z'; $example->orders[] = $order; - $this->setExpectedException(get_class(new ObjectException()), 'Required property missing: id, data: {"dateTime":"2015-10-28T07:28:00Z"} at #->$ref[#/definitions/user]->properties:orders->items[0]:0->$ref[#/definitions/order]'); + $this->setExpectedException(get_class(new ObjectException()), 'Required property missing: id, data: {"date_time":"2015-10-28T07:28:00Z"} at #->$ref[#/definitions/user]->properties:orders->items[0]:0->$ref[#/definitions/order]'); /** @noinspection PhpUnhandledExceptionInspection */ User::export($example); // Exception: Required property missing: id, data: {"dateTime":"2015-10-28T07:28:00Z"} at #->$ref[#/definitions/user]->properties:orders->items[0]:0->$ref[#/definitions/order] } @@ -150,6 +150,15 @@ public function testNameMapper() $this->assertSame('2015-10-28T07:28:00Z', $imported->dateTime); $this->assertSame(2.2, $imported->price); + } + + public function testNameMapperNonDefault() + { + $order = new Order(); + $order->id = 1; + $order->dateTime = '2015-10-28T07:28:00Z'; + $order->price = 2.2; + $options = new Context(); $options->mapping = Order::FANCY_MAPPING; @@ -168,7 +177,6 @@ public function testNameMapper() $this->assertSame('2015-10-28T07:28:00Z', $imported->dateTime); } - public function testNestedStructure() { $user = new User(); diff --git a/tests/src/PHPUnit/Suite/Issue63Test.php b/tests/src/PHPUnit/Suite/Issue63Test.php new file mode 100644 index 0000000..c96dfd0 --- /dev/null +++ b/tests/src/PHPUnit/Suite/Issue63Test.php @@ -0,0 +1,36 @@ +setId(123); + $user->setFirstName('first'); + $user->setLastName('last'); + $user->setAge(10); + + $this->assertSame( + '{"id":123,"first_name":"first","last_name":"last","age":10}', + json_encode(User63::export($user)) + ); + + // no exception expected + $user->validate(); + + + $this->assertSame( + '{"id":123,"first_name":"first","last_name":"last","age":10}', + json_encode($user->jsonSerialize()) + ); + + $this->assertSame( + '{"id":123,"first_name":"first","last_name":"last","age":10}', + json_encode($user) + ); + } +} \ No newline at end of file