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  if ( $expectedClass && $class !== $expectedClass && !is_subclass_of( $class, $expectedClass ) ) {
70  throw new InvalidArgumentException(
71  "Refusing to unserialize: expected $expectedClass, got $class"
72  );
73  }
74  return $class::newFromJsonArray( $this, $json );
75  }
76 
77  public function unserializeArray( array $array ): array {
78  $unserializedExtensionData = [];
79  foreach ( $array as $key => $value ) {
80  if ( $this->canMakeNewFromValue( $value ) ) {
81  $unserializedExtensionData[$key] = $this->unserialize( $value );
82  } else {
83  $unserializedExtensionData[$key] = $value;
84  }
85  }
86  return $unserializedExtensionData;
87  }
88 
89  public function serialize( $value ) {
90  if ( $value instanceof JsonSerializable ) {
91  $value = $value->jsonSerialize();
92  }
93  $json = FormatJson::encode( $value, false, FormatJson::ALL_OK );
94  if ( !$json ) {
95  // TODO: make it JsonException
96  throw new InvalidArgumentException(
97  'Failed to encode JSON. Error ' . json_last_error_msg()
98  );
99  }
100 
101  // Detect if the array contained any properties non-serializable
102  // to json. We will not be able to deserialize the value correctly
103  // anyway, so return null. This is done after calling FormatJson::encode
104  // to avoid walking over circular structures.
105  // TODO: make detectNonSerializableData not choke on cyclic structures.
106  $unserializablePath = $this->detectNonSerializableData( $value, true );
107  if ( $unserializablePath ) {
108  // TODO: Make it JsonException
109  throw new InvalidArgumentException(
110  "Non-unserializable property set at {$unserializablePath}"
111  );
112  }
113 
114  return $json;
115  }
116 
122  private function canMakeNewFromValue( $json ): bool {
123  $classAnnotation = JsonConstants::TYPE_ANNOTATION;
124  if ( is_array( $json ) ) {
125  return array_key_exists( $classAnnotation, $json );
126  }
127 
128  if ( is_object( $json ) ) {
129  return $json->$classAnnotation;
130  }
131  return false;
132  }
133 
143  $value,
144  bool $expectUnserialize,
145  string $accumulatedPath
146  ): ?string {
147  if ( is_array( $value ) ||
148  ( is_object( $value ) && get_class( $value ) === 'stdClass' ) ) {
149  foreach ( $value as $key => $propValue ) {
150  $propValueNonSerializablePath = $this->detectNonSerializableDataInternal(
151  $propValue,
152  $expectUnserialize,
153  $accumulatedPath . '.' . $key
154  );
155  if ( $propValueNonSerializablePath ) {
156  return $propValueNonSerializablePath;
157  }
158  }
159  } elseif ( ( $expectUnserialize && $value instanceof JsonUnserializable )
160  // Trust that JsonSerializable will correctly serialize.
161  || ( !$expectUnserialize && $value instanceof JsonSerializable )
162  ) {
163  return null;
164  // Instances of classes other the \stdClass or JsonSerializable can not be serialized to JSON.
165  } elseif ( !is_scalar( $value ) && $value !== null ) {
166  return $accumulatedPath;
167  }
168  return null;
169  }
170 
181  public function detectNonSerializableData( $value, bool $expectUnserialize = false ): ?string {
182  return $this->detectNonSerializableDataInternal( $value, $expectUnserialize, '$' );
183  }
184 }
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:142
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:181
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:96
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:89
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:122
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:160
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:77
MediaWiki\Json\JsonCodec
Definition: JsonCodec.php:36
MediaWiki\Json\JsonSerializer
Definition: JsonSerializer.php:33