MediaWiki  master
JsonCodec.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Json;
23 
24 use FormatJson;
25 use InvalidArgumentException;
26 use JsonSerializable;
27 use Wikimedia\Assert\Assert;
28 
37 
38  public function unserialize( $json, string $expectedClass = null ) {
39  Assert::parameterType( 'object|array|string', $json, '$json' );
40  Assert::precondition(
41  !$expectedClass || is_subclass_of( $expectedClass, JsonUnserializable::class ),
42  '$expectedClass parameter must be subclass of JsonUnserializable, got ' . $expectedClass
43  );
44  if ( is_string( $json ) ) {
45  $jsonStatus = FormatJson::parse( $json, FormatJson::FORCE_ASSOC );
46  if ( !$jsonStatus->isGood() ) {
47  // TODO: in PHP 7.3, we can use JsonException
48  throw new InvalidArgumentException( "Bad JSON: {$jsonStatus}" );
49  }
50  $json = $jsonStatus->getValue();
51  }
52 
53  if ( is_object( $json ) ) {
54  $json = (array)$json;
55  }
56 
57  if ( !$this->canMakeNewFromValue( $json ) ) {
58  if ( $expectedClass ) {
59  throw new InvalidArgumentException( 'JSON did not have ' . JsonConstants::TYPE_ANNOTATION );
60  }
61  return $json;
62  }
63 
64  $class = $json[JsonConstants::TYPE_ANNOTATION];
65  if ( !class_exists( $class ) || !is_subclass_of( $class, JsonUnserializable::class ) ) {
66  throw new InvalidArgumentException( "Invalid target class {$class}" );
67  }
68 
69  $obj = $class::newFromJsonArray( $this, $json );
70 
71  // Check we haven't accidentally unserialized a godzilla if we were told we are not expecting it.
72  if ( $expectedClass && !$obj instanceof $expectedClass ) {
73  $actualClass = get_class( $obj );
74  throw new InvalidArgumentException( "Expected {$expectedClass}, got {$actualClass}" );
75  }
76  return $obj;
77  }
78 
79  public function unserializeArray( array $array ) : array {
80  $unserializedExtensionData = [];
81  foreach ( $array as $key => $value ) {
82  if ( $this->canMakeNewFromValue( $value ) ) {
83  $unserializedExtensionData[$key] = $this->unserialize( $value );
84  } else {
85  $unserializedExtensionData[$key] = $value;
86  }
87  }
88  return $unserializedExtensionData;
89  }
90 
91  public function serialize( $value ) {
92  if ( $value instanceof JsonSerializable ) {
93  $value = $value->jsonSerialize();
94  }
95  $json = FormatJson::encode( $value, false, FormatJson::ALL_OK );
96  if ( !$json ) {
97  // TODO: make it JsonException
98  throw new InvalidArgumentException(
99  'Failed to encode JSON. Error ' . json_last_error_msg()
100  );
101  }
102 
103  // Detect if the array contained any properties non-serializable
104  // to json. We will not be able to deserialize the value correctly
105  // anyway, so return null. This is done after calling FormatJson::encode
106  // to avoid walking over circular structures.
107  // TODO: make detectNonSerializableData not choke on cyclic structures.
108  $unserializablePath = $this->detectNonSerializableData( $value, true );
109  if ( $unserializablePath ) {
110  // TODO: Make it JsonException
111  throw new InvalidArgumentException(
112  "Non-unserializable property set at {$unserializablePath}"
113  );
114  }
115 
116  return $json;
117  }
118 
124  private function canMakeNewFromValue( $json ) : bool {
125  $classAnnotation = JsonConstants::TYPE_ANNOTATION;
126  if ( is_array( $json ) ) {
127  return array_key_exists( $classAnnotation, $json );
128  }
129 
130  if ( is_object( $json ) ) {
131  return $json->$classAnnotation;
132  }
133  return false;
134  }
135 
145  $value,
146  bool $expectUnserialize,
147  string $accumulatedPath
148  ) : ?string {
149  if ( is_array( $value ) ||
150  ( is_object( $value ) && get_class( $value ) === 'stdClass' ) ) {
151  foreach ( $value as $key => $propValue ) {
152  $propValueNonSerializablePath = $this->detectNonSerializableDataInternal(
153  $propValue,
154  $expectUnserialize,
155  $accumulatedPath . '.' . $key
156  );
157  if ( $propValueNonSerializablePath ) {
158  return $propValueNonSerializablePath;
159  }
160  }
161  } elseif ( ( $expectUnserialize && $value instanceof JsonUnserializable )
162  // Trust that JsonSerializable will correctly serialize.
163  || ( !$expectUnserialize && $value instanceof JsonSerializable )
164  ) {
165  return null;
166  // Instances of classes other the \stdClass or JsonSerializable can not be serialized to JSON.
167  } elseif ( !is_scalar( $value ) && $value !== null ) {
168  return $accumulatedPath;
169  }
170  return null;
171  }
172 
183  public function detectNonSerializableData( $value, bool $expectUnserialize = false ) : ?string {
184  return $this->detectNonSerializableDataInternal( $value, $expectUnserialize, '$' );
185  }
186 }
MediaWiki\Json\JsonUnserializer
Definition: JsonUnserializer.php:33
MediaWiki\Json\JsonCodec\detectNonSerializableDataInternal
detectNonSerializableDataInternal( $value, bool $expectUnserialize, string $accumulatedPath)
Recursive check for ability to serialize $value to JSON via FormatJson::encode().
Definition: JsonCodec.php:144
MediaWiki\Json\JsonUnserializable
Definition: JsonUnserializable.php:38
MediaWiki\Json\JsonCodec\detectNonSerializableData
detectNonSerializableData( $value, bool $expectUnserialize=false)
Checks if the $value is JSON-serializable (contains only scalar values) and returns a JSON-path to th...
Definition: JsonCodec.php:183
MediaWiki\Json
Definition: JsonCodec.php:22
FormatJson\ALL_OK
const ALL_OK
Skip escaping as many characters as reasonably possible.
Definition: FormatJson.php:55
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
FormatJson
JSON formatter wrapper class.
Definition: FormatJson.php:26
MediaWiki\Json\JsonCodec\serialize
serialize( $value)
Encode $value as JSON with an intent to use JsonUnserializer::unserialize to decode it back.
Definition: JsonCodec.php:91
FormatJson\FORCE_ASSOC
const FORCE_ASSOC
If set, treat JSON objects '{...}' as associative arrays.
Definition: FormatJson.php:63
MediaWiki\Json\JsonCodec\canMakeNewFromValue
canMakeNewFromValue( $json)
Is it likely possible to make a new instance from $json serialization?
Definition: JsonCodec.php:124
MediaWiki\Json\JsonConstants\TYPE_ANNOTATION
const TYPE_ANNOTATION
Name of the property where the class information is stored.
Definition: JsonConstants.php:34
FormatJson\parse
static parse( $value, $options=0)
Decodes a JSON string.
Definition: FormatJson.php:188
MediaWiki\Json\JsonCodec\unserialize
unserialize( $json, string $expectedClass=null)
Restore an instance of simple type or JsonUnserializable subclass from the JSON serialization.
Definition: JsonCodec.php:38
MediaWiki\Json\JsonCodec\unserializeArray
unserializeArray(array $array)
Helper to unserialize an array of JsonUnserializable instances or simple types.
Definition: JsonCodec.php:79
MediaWiki\Json\JsonCodec
Definition: JsonCodec.php:36
MediaWiki\Json\JsonSerializer
Definition: JsonSerializer.php:33