Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F2751949
JsonCodec.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
19 KB
Referenced Files
None
Subscribers
None
JsonCodec.php
View Options
<?php
declare
(
strict_types
=
1
);
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace
Wikimedia\JsonCodec
;
use
Exception
;
use
InvalidArgumentException
;
use
Psr\Container\ContainerInterface
;
use
Psr\Container\NotFoundExceptionInterface
;
use
ReflectionClass
;
use
stdClass
;
/**
* Helper class to serialize/unserialize things to/from JSON.
*/
class
JsonCodec
implements
JsonCodecInterface
{
/** @var ContainerInterface Service container */
protected
ContainerInterface
$serviceContainer
;
/** @var array<class-string,JsonClassCodec> Class codecs */
protected
array
$codecs
;
/**
* Name of the property where class information is stored; it also
* is used to mark "complex" arrays, and as a place to store the contents
* of any pre-existing array property that happened to have the same name.
*/
protected
const
TYPE_ANNOTATION
=
'_type_'
;
/**
* @param ?ContainerInterface $serviceContainer
*/
public
function
__construct
(
?
ContainerInterface
$serviceContainer
=
null
)
{
$this
->
serviceContainer
=
$serviceContainer
??
// Use an empty container if none is provided.
new
class
implements
ContainerInterface
{
/**
* @param string $id
* @return never
*/
public
function
get
(
$id
)
{
throw
new
class
(
"not found"
)
extends
Exception
implements
NotFoundExceptionInterface
{
};
}
/** @inheritDoc */
public
function
has
(
string
$id
):
bool
{
return
false
;
}
};
$this
->
addCodecFor
(
stdClass
::
class
,
JsonStdClassCodec
::
getInstance
()
);
}
/**
* Recursively converts a given object to a JSON-encoded string.
* While serializing the $value JsonCodec delegates to the appropriate
* JsonClassCodecs of any classes which implement JsonCodecable.
*
* If a $classHint is provided and matches the type of the value,
* then type information will not be included in the generated JSON;
* otherwise an appropriate class name will be added to the JSON to
* guide deserialization.
*
* @param mixed|null $value
* @param class-string|Hint|null $classHint An optional hint to
* the type of the encoded object. If this is provided and matches
* the type of $value, then explicit type information will be omitted
* from the generated JSON, which saves some space.
* @return string
*/
public
function
toJsonString
(
$value
,
$classHint
=
null
):
string
{
return
json_encode
(
$this
->
toJsonArray
(
$value
,
$classHint
),
JSON_UNESCAPED_SLASHES
|
JSON_UNESCAPED_UNICODE
|
JSON_HEX_TAG
|
JSON_HEX_AMP
);
}
/**
* Recursively converts a JSON-encoded string to an object value or scalar.
* While deserializing the $json JsonCodec delegates to the appropriate
* JsonClassCodecs of any classes which implement JsonCodecable.
*
* For objects encoded using implicit class information, a "class hint"
* can be provided to guide deserialization; this is unnecessary for
* objects serialized with explicit classes.
*
* @param string $json A JSON-encoded string
* @param class-string|Hint|null $classHint An optional hint to
* the type of the encoded object. In the absence of explicit
* type information in the JSON, this will be used as the type of
* the created object.
* @return mixed|null
*/
public
function
newFromJsonString
(
$json
,
$classHint
=
null
)
{
return
$this
->
newFromJsonArray
(
json_decode
(
$json
,
true
),
$classHint
);
}
/**
* Maintain a cache giving the codec for a given class name.
*
* Reusing this JsonCodec object will also reuse this cache, which
* could improve performance somewhat.
*
* @param class-string $className
* @return ?JsonClassCodec a codec for the class, or null if the class is
* not serializable.
*/
protected
function
codecFor
(
string
$className
):
?
JsonClassCodec
{
$codec
=
$this
->
codecs
[
$className
]
??
null
;
if
(
$codec
!==
null
)
{
return
$codec
;
}
// Check for class aliases to ensure we don't use split codecs
$trueName
=
(
new
ReflectionClass
(
$className
)
)->
getName
();
if
(
$trueName
!==
$className
)
{
$codec
=
$this
->
codecs
[
$trueName
]
??
null
;
if
(
$codec
!==
null
)
{
$this
->
codecs
[
$className
]
=
$codec
;
return
$codec
;
}
$className
=
$trueName
;
}
if
(
is_a
(
$className
,
JsonCodecable
::
class
,
true
)
)
{
$codec
=
$className
::
jsonClassCodec
(
$this
,
$this
->
serviceContainer
);
$this
->
codecs
[
$className
]
=
$codec
;
}
return
$codec
;
}
/**
* Allow the use of a customized encoding for the given class; the given
* className need not be a JsonCodecable and if it *does* correspond to
* a JsonCodecable it will override the class codec specified by the
* JsonCodecable.
* @param class-string $className
* @param JsonClassCodec $codec A codec to use for $className
*/
public
function
addCodecFor
(
string
$className
,
JsonClassCodec
$codec
):
void
{
// Resolve aliases
$className
=
(
new
ReflectionClass
(
$className
)
)->
getName
();
// Sanity check
if
(
isset
(
$this
->
codecs
[
$className
]
)
)
{
throw
new
InvalidArgumentException
(
"Codec already present for $className"
);
}
$this
->
codecs
[
$className
]
=
$codec
;
}
/**
* Recursively converts a given object to an associative array
* which can be json-encoded. (When embedding an object into
* another context it is sometimes useful to have the array
* representation rather than the string JSON form of the array;
* this can also be useful if you want to pretty-print the result,
* etc.) While converting $value the JsonCodec delegates to the
* appropriate JsonClassCodecs of any classes which implement
* JsonCodecable.
*
* If a $classHint is provided and matches the type of the value,
* then type information will not be included in the generated JSON;
* otherwise an appropriate class name will be added to the JSON to
* guide deserialization.
*
* @param mixed|null $value
* @param class-string|Hint|null $classHint An optional hint to
* the type of the encoded object. If this is provided and matches
* the type of $value, then explicit type information will be omitted
* from the generated JSON, which saves some space.
* @return mixed|null
*/
public
function
toJsonArray
(
$value
,
$classHint
=
null
)
{
$is_complex
=
false
;
$className
=
'array'
;
$codec
=
null
;
$classHintCodec
=
null
;
// Process class hints
$arrayClassHint
=
null
;
$forceBraces
=
null
;
$allowInherited
=
false
;
if
(
is_string
(
$classHint
)
&&
str_ends_with
(
$classHint
,
'[]'
)
)
{
// back-compat
$classHint
=
new
Hint
(
substr
(
$classHint
,
0
,
-
2
),
Hint
::
LIST
);
}
while
(
$classHint
instanceof
Hint
)
{
if
(
$classHint
->
modifier
===
Hint
::
USE_SQUARE
)
{
// Allow list-like serializations to use []
$classHint
=
$classHint
->
parent
;
$forceBraces
=
false
;
}
elseif
(
$classHint
->
modifier
===
Hint
::
ALLOW_OBJECT
)
{
// Force empty arrays to serialize as {}
$classHint
=
$classHint
->
parent
;
$forceBraces
=
true
;
}
elseif
(
$classHint
->
modifier
===
Hint
::
LIST
)
{
// Array whose values are the hinted type
$arrayClassHint
=
$classHint
->
parent
;
$classHint
=
'array'
;
}
elseif
(
$classHint
->
modifier
===
Hint
::
STDCLASS
)
{
// stdClass whose values are the hinted type
$arrayClassHint
=
$classHint
->
parent
;
$classHint
=
stdClass
::
class
;
}
elseif
(
$classHint
->
modifier
===
Hint
::
INHERITED
)
{
// Allow the hint to match subclasses of the hinted class
$classHint
=
$classHint
->
parent
;
$allowInherited
=
true
;
}
elseif
(
$classHint
->
modifier
===
Hint
::
DEFAULT
)
{
// No-op, included for completeness
$classHint
=
$classHint
->
parent
;
}
else
{
throw
new
InvalidArgumentException
(
'bad hint modifier: '
.
$classHint
->
modifier
);
}
}
if
(
is_object
(
$value
)
)
{
$className
=
get_class
(
$value
);
$codec
=
$this
->
codecFor
(
$className
);
if
(
$codec
!==
null
)
{
$value
=
$codec
->
toJsonArray
(
$value
);
$is_complex
=
true
;
}
// Tweak the codec used for class hints if $allowInherited is true
// in order to match the codec we would use for deserialization.
if
(
$allowInherited
&&
$classHint
!==
null
&&
is_a
(
$className
,
$classHint
,
true
)
&&
// extra comparison to let phan know $classHint is not 'array'
$classHint
!==
'array'
)
{
$className
=
$classHint
;
$codec
=
$this
->
codecFor
(
$className
);
}
}
elseif
(
is_array
(
$value
)
&&
$this
->
isArrayMarked
(
$value
)
)
{
$is_complex
=
true
;
}
if
(
is_array
(
$value
)
)
{
// Recursively convert array values to serializable form
foreach
(
$value
as
$key
=>
&
$v
)
{
if
(
is_object
(
$v
)
||
is_array
(
$v
)
)
{
$propClassHint
=
$arrayClassHint
;
$propClassHint
??=
(
$codec
===
null
?
null
:
// phan can't tell that $codec is null when $className is 'array'
// @phan-suppress-next-line PhanUndeclaredClassReference
$codec
->
jsonClassHintFor
(
$className
,
(
string
)
$key
)
);
$v
=
$this
->
toJsonArray
(
$v
,
$propClassHint
);
if
(
$propClassHint
!==
null
||
$this
->
isArrayMarked
(
$v
)
)
{
// an array which contains complex components is
// itself complex.
$is_complex
=
true
;
}
}
}
// Ok, now mark the array, being careful to transfer away
// any fields with the same names as our markers.
if
(
$is_complex
||
$classHint
!==
null
)
{
if
(
$forceBraces
===
null
&&
$className
!==
'array'
&&
array_is_list
(
$value
)
)
{
// Include the type annotation (by clearing the
// hint) if $forceBraces isn't false and it is
// necessary to break up a list. This ensures that
// all objects have a JSON encoding in the `{...}`
// style, even if they happen to have all-numeric
// keys.
$classHint
=
null
;
}
// Even if $className === $classHint we may need to record this
// array as "complex" (ie, requires recursion to process
// individual values during deserialization)
// @phan-suppress-next-line PhanUndeclaredClassReference 'array'
$this
->
markArray
(
$value
,
$className
,
$classHint
);
if
(
$forceBraces
===
true
&&
array_is_list
(
$value
)
)
{
// It is somewhat surprising for ::toJsonArray() to return
// an object (rather than an array), but allow this case
// if the class hint expressly asked for it.
$value
=
(
object
)
$value
;
}
}
}
elseif
(
!
is_scalar
(
$value
)
&&
$value
!==
null
)
{
throw
new
InvalidArgumentException
(
'Unable to serialize JSON: '
.
get_debug_type
(
$value
)
);
}
return
$value
;
}
/**
* Recursively converts an associative array (or scalar) to an
* object value (or scalar). While converting this value JsonCodec
* delegates to the appropriate JsonClassCodecs of any classes which
* implement JsonCodecable.
*
* For objects encoded using implicit class information, a "class hint"
* can be provided to guide deserialization; this is unnecessary for
* objects serialized with explicit classes.
*
* @param mixed|null $json
* @param class-string|Hint|null $classHint An optional hint to
* the type of the encoded object. In the absence of explicit
* type information in the JSON, this will be used as the type of
* the created object.
* @return mixed|null
*/
public
function
newFromJsonArray
(
$json
,
$classHint
=
null
)
{
if
(
$json
instanceof
stdClass
)
{
// We *shouldn't* be given an object... but we might.
$json
=
(
array
)
$json
;
}
// Process class hints
$arrayClassHint
=
null
;
if
(
is_string
(
$classHint
)
&&
str_ends_with
(
$classHint
,
'[]'
)
)
{
// back-compat
$classHint
=
new
Hint
(
substr
(
$classHint
,
0
,
-
2
),
Hint
::
LIST
);
}
while
(
$classHint
instanceof
Hint
)
{
if
(
$classHint
->
modifier
===
Hint
::
LIST
)
{
$arrayClassHint
=
$classHint
->
parent
;
$classHint
=
'array'
;
}
elseif
(
$classHint
->
modifier
===
Hint
::
STDCLASS
)
{
$arrayClassHint
=
$classHint
->
parent
;
$classHint
=
stdClass
::
class
;
}
else
{
$classHint
=
$classHint
->
parent
;
}
}
// Is this an array containing a complex value?
if
(
is_array
(
$json
)
&&
(
$this
->
isArrayMarked
(
$json
)
||
$classHint
!==
null
)
)
{
// Read out our metadata
// @phan-suppress-next-line PhanUndeclaredClassReference 'array'
$className
=
$this
->
unmarkArray
(
$json
,
$classHint
);
// Create appropriate codec
$codec
=
null
;
if
(
$className
!==
'array'
)
{
$codec
=
$this
->
codecFor
(
$className
);
if
(
$codec
===
null
)
{
throw
new
InvalidArgumentException
(
"Unable to deserialize JSON for $className"
);
}
}
// Recursively unserialize the array contents.
$unserialized
=
[];
foreach
(
$json
as
$key
=>
$value
)
{
$propClassHint
=
$arrayClassHint
;
$propClassHint
??=
(
$codec
===
null
?
null
:
// phan can't tell that $codec is null when $className is 'array'
// @phan-suppress-next-line PhanUndeclaredClassReference
$codec
->
jsonClassHintFor
(
$className
,
(
string
)
$key
)
);
if
(
$value
instanceof
stdClass
)
{
// Again, we *shouldn't* be given an object... but we might.
$value
=
(
array
)
$value
;
}
if
(
is_array
(
$value
)
&&
(
$this
->
isArrayMarked
(
$value
)
||
$propClassHint
!==
null
)
)
{
$unserialized
[
$key
]
=
$this
->
newFromJsonArray
(
$value
,
$propClassHint
);
}
else
{
$unserialized
[
$key
]
=
$value
;
}
}
// Use a JsonCodec to create the object instance if appropriate.
if
(
$className
===
'array'
)
{
$json
=
$unserialized
;
}
else
{
$json
=
$codec
->
newFromJsonArray
(
$className
,
$unserialized
);
}
}
return
$json
;
}
// Functions to mark/unmark arrays and record a class name using a
// single reserved field, named by self::TYPE_ANNOTATION. A
// subclass can provide alternate implementations of these methods
// if it wants to use a different reserved field or else wishes to
// reserve more fields/encode certain types more compactly/flag
// certain types of values. For example: a subclass could choose
// to discard all hints in `markArray` in order to explicitly mark
// all types in preparation for a format change; or all values of
// type DocumentFragment might get a marker flag added so they can
// be identified without knowledge of the class hint; or perhaps a
// separate schema can be used to record class names more
// compactly.
/**
* Determine if the given value is "marked"; that is, either
* represents a object type encoded using a JsonClassCodec or else
* is an array which contains values (or contains arrays
* containing values, etc) which are object types. The values of
* unmarked arrays are not decoded, in order to speed up the
* decoding process. Arrays may also be marked even if they do
* not represent object types (or an array recursively containing
* them) if they contain keys that need to be escaped ("false
* marks"); as such this method is called both on the raw results
* of JsonClassCodec (to check for "false marks") as well as on
* encoded arrays (to find "true marks").
*
* Arrays do not have to be marked if the decoder has a class hint.
*
* @param array $value An array result from `JsonClassCodec::toJsonArray()`,
* or an array result from `::markArray()`
* @return bool Whether the $value is marked
*/
protected
function
isArrayMarked
(
array
$value
):
bool
{
return
array_key_exists
(
self
::
TYPE_ANNOTATION
,
$value
);
}
/**
* Record a mark in the array, reversibly.
*
* The mark should record the class name, if it is different from
* the class hint. The result does not need to trigger
* `::isArrayMarked` if there is an accurate class hint present,
* but otherwise the result should register as marked. The
* provided value may be a "complex" array (one that recursively
* contains encoded object) or an array with a "false mark"; in
* both cases the provided $className will be `array`.
*
* @param array &$value An array result from `JsonClassCodec::toJsonArray()`
* or a "complex" array
* @param 'array'|class-string $className The name of the class encoded
* by the codec, or else `array` if $value is a "complex" array or a
* "false mark"
* @param 'array'|class-string|null $classHint The class name provided as
* a hint to the encoder, and which will be in turn provided as a hint
* to the decoder, or `null` if no hint was provided. The class hint
* will be `array` when the array is a homogeneous list of objects.
*/
protected
function
markArray
(
array
&
$value
,
string
$className
,
?
string
$classHint
):
void
{
// We're going to use an array key, but first we have to see whether it
// was already present in the array we've been given, in which case
// we need to escape it (by hoisting into a child array).
if
(
array_key_exists
(
self
::
TYPE_ANNOTATION
,
$value
)
)
{
// @phan-suppress-next-line PhanUndeclaredClassReference 'array'
if
(
!
self
::
classEquals
(
$className
,
$classHint
)
)
{
$value
[
self
::
TYPE_ANNOTATION
]
=
[
$value
[
self
::
TYPE_ANNOTATION
],
$className
];
}
else
{
// Omit $className since it matches the $classHint, but we still
// need to escape the field to make it clear it was marked.
// (If the class hint hadn't matched, the proper class name
// would be here in an array, and we need to distinguish that
// case from the case where the "actual value" is an array.)
$value
[
self
::
TYPE_ANNOTATION
]
=
[
$value
[
self
::
TYPE_ANNOTATION
]
];
}
}
elseif
(
// @phan-suppress-next-line PhanUndeclaredClassReference 'array'
!
self
::
classEquals
(
$className
,
$classHint
)
)
{
// Include the type annotation if it doesn't match the hint
$value
[
self
::
TYPE_ANNOTATION
]
=
$className
;
}
}
/**
* Remove a mark from an encoded array, and return an
* encoded class name if present.
*
* The provided array may not trigger `::isArrayMarked` is there
* was a class hint provided.
*
* If the provided array had a "false mark" or recursively
* contained objects, the returned class name should be 'array'.
*
* @param array &$value An encoded array
* @param 'array'|class-string|null $classHint The class name provided as a hint to
* the decoder, which was previously provided as a hint to the encoder,
* or `null` if no hint was provided.
* @return 'array'|class-string The class name to be used for decoding, or
* 'array' if the value was a "complex" or "false mark" array.
*/
protected
function
unmarkArray
(
array
&
$value
,
?
string
$classHint
):
string
{
$className
=
$value
[
self
::
TYPE_ANNOTATION
]
??
$classHint
;
// Remove our marker and restore the previous state of the
// json array (restoring a pre-existing field if needed)
if
(
is_array
(
$className
)
)
{
$value
[
self
::
TYPE_ANNOTATION
]
=
$className
[
0
];
$className
=
$className
[
1
]
??
$classHint
;
}
else
{
unset
(
$value
[
self
::
TYPE_ANNOTATION
]
);
}
return
$className
;
}
/**
* Helper function to test two class strings for equality in the presence
* of class aliases.
*
* @param 'array'|class-string $class1
* @param 'array'|class-string|null $class2
* @return bool True if the arguments refer to the same class
*/
private
static
function
classEquals
(
string
$class1
,
?
string
$class2
):
bool
{
if
(
$class1
===
$class2
)
{
// Fast path
return
true
;
}
if
(
$class2
===
null
||
$class1
===
'array'
||
$class2
===
'array'
)
{
return
false
;
}
if
(
is_a
(
$class1
,
$class2
,
true
)
&&
is_a
(
$class2
,
$class1
,
true
)
)
{
// Check aliases
return
true
;
}
return
false
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Jul 3, 19:21 (1 d, 13 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
7e/f0/3e00af8cb782e5659a548e26f9de
Default Alt Text
JsonCodec.php (19 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment