JSON object structure from json_decode()
* @param object|string $object
* Object to map $json data into
*
* @return mixed
* mapped object is returned
*
* @see mapArray()
*
* @template TObject
* @phpstan-param TObject|class-string $object
* Object to map $json data into.
* @phpstan-return TObject
*
*/
public function map($json, $object)
{
if (\is_string($object) && \class_exists($object)) {
$object = self::createInstance($object);
}
if (!\is_object($object)) {
throw new \InvalidArgumentException(
'JsonMapper::map() requires second argument to be an object, ' . \gettype($object) . ' given.'
);
}
$strClassName = \get_class($object);
$rc = new \ReflectionClass($object);
$strNs = $rc->getNamespaceName();
foreach ($json as $key => $jsonValue) {
$key = $this->getSafeName($key);
// Store the property inspection results, so we don't have to do it
// again for subsequent objects of the same type.
if (!isset($this->arInspectedClasses[$strClassName][$key])) {
$this->arInspectedClasses[$strClassName][$key] = $this->inspectProperty($rc, $key);
}
list(
$hasProperty,
$accessor,
$type
) = $this->arInspectedClasses[$strClassName][$key];
if (!$hasProperty) {
if (\is_callable($this->undefinedPropertyHandler)) {
\call_user_func(
$this->undefinedPropertyHandler,
$object,
$key,
$jsonValue
);
}
continue;
}
if ($accessor === null) {
continue;
}
if ($this->isNullable($type)) {
if ($jsonValue === null) {
$this->setProperty($object, $accessor, null);
continue;
}
$type = $this->removeNullable($type);
} elseif ($jsonValue === null) {
throw new \InvalidArgumentException(
'JSON property "' . $key . '" in class "' . $strClassName . '" must not be NULL'
);
}
$type = $this->getFullNamespace($type, $strNs);
$type = $this->getMappedType($type, $jsonValue);
if (
$type === null
||
$type === 'mixed'
) {
// no given type - simply set the json data
$this->setProperty($object, $accessor, $jsonValue);
continue;
}
if ($this->isObjectOfSameType($type, $jsonValue)) {
$this->setProperty($object, $accessor, $jsonValue);
continue;
}
if ($this->isSimpleType($type)) {
if ($type === 'string' && \is_object($jsonValue)) {
throw new \InvalidArgumentException(
'JSON property "' . $key . '" in class "' . $strClassName . '" is an object and cannot be converted to a string'
);
}
if (\strpos($type, '|') !== false) {
foreach (\explode('|', $type) as $tmpType) {
if (\gettype($jsonValue) === $tmpType) {
\settype($jsonValue, $tmpType);
}
}
} else {
\settype($jsonValue, $type);
}
$this->setProperty($object, $accessor, $jsonValue);
continue;
}
if ($type === '') {
throw new \InvalidArgumentException(
'Empty type at property "' . $strClassName . '::$' . $key . '"'
);
}
$array = null;
$subtype = null;
if ($this->isArrayOfType($type)) {
$array = [];
$subtype = \substr($type, 0, -2);
} elseif (\substr($type, -1) == ']') {
list($proptype, $subtype) = \explode('[', \substr($type, 0, -1));
if ($proptype == 'array') {
$array = [];
} else {
/** @noinspection PhpSillyAssignmentInspection - phpstan helper */
/** @phpstan-var class-string $proptype */
$proptype = $proptype;
$array = self::createInstance($proptype, false, $jsonValue);
}
} elseif (\is_a($type, \ArrayObject::class, true)) {
/** @noinspection PhpSillyAssignmentInspection - phpstan helper */
/** @phpstan-var \ArrayObject $type */
$type = $type;
$array = self::createInstance($type, false, $jsonValue);
}
if ($array !== null) {
/** @noinspection NotOptimalIfConditionsInspection */
if (
!\is_array($jsonValue)
&&
$this->isScalarType(\gettype($jsonValue))
) {
throw new \InvalidArgumentException(
'JSON property "' . $key . '" must be an array, ' . \gettype($jsonValue) . ' given'
);
}
$cleanSubtype = $this->removeNullable($subtype);
$subtype = $this->getFullNamespace($cleanSubtype, $strNs);
$child = $this->mapArray($jsonValue, $array, $subtype, $key);
} elseif ($this->isScalarType(\gettype($jsonValue))) {
// use constructor parameter if we have a class, but only a flat type (i.e. string, int)
/** @noinspection PhpSillyAssignmentInspection - phpstan helper */
/** @phpstan-var object $type */
$type = $type;
$child = self::createInstance($type, true, $jsonValue);
} else {
/** @noinspection PhpSillyAssignmentInspection - phpstan helper */
/** @phpstan-var object $type */
$type = $type;
$child = self::createInstance($type, false, $jsonValue);
$this->map($jsonValue, $child);
}
$this->setProperty($object, $accessor, $child);
}
/** @noinspection PhpSillyAssignmentInspection */
/** @phpstan-var TObject $object */
$object = $object;
return $object;
}
/**
* Map an array
*
* @param array $json JSON array structure from json_decode()
* @param mixed $array Array or ArrayObject that gets filled with
* data from $json
* @param string|null $class Class name for children objects.
* All children will get mapped onto this type.
* Supports class names and simple types
* like "string" and nullability "string|null".
* Pass "null" to not convert any values
* @param string $parent_key defines the key this array belongs to
* in order to aid debugging
*
* @pslam-param null|class-string $class
*
* @return mixed Mapped $array is returned
*/
public function mapArray($json, $array, $class = null, $parent_key = '')
{
$originalClass = $class;
foreach ($json as $key => $jsonValue) {
$class = $this->getMappedType($originalClass, $jsonValue);
if ($class === null) {
$foundArrayy = false;
if ($array instanceof \Arrayy\Arrayy && $jsonValue instanceof \stdClass) {
foreach ($array->getPhpDocPropertiesFromClass() as $typesKey => $typesTmp) {
if (
(
$typesKey === $key
||
$typesKey === \Arrayy\Arrayy::ARRAYY_HELPER_TYPES_FOR_ALL_PROPERTIES
)
&&
\count($typesTmp->getTypes()) === 1
&&
\is_subclass_of($typesTmp->getTypes()[0], \Arrayy\Arrayy::class)
) {
$array[$key] = $typesTmp->getTypes()[0]::createFromObjectVars($jsonValue);
$foundArrayy = true;
break;
}
}
}
if ($foundArrayy === false) {
if ($array instanceof \Arrayy\Arrayy && $jsonValue instanceof \stdClass) {
foreach ($array->getPhpDocPropertiesFromClass() as $typesKey => $typesTmp) {
if (
(
$typesKey === $key
||
$typesKey === \Arrayy\Arrayy::ARRAYY_HELPER_TYPES_FOR_ALL_PROPERTIES
)
&&
\count($typesTmp->getTypes()) === 1
) {
$array[$key] = $this->map($jsonValue, $typesTmp->getTypes()[0]);
$foundArrayy = true;
break;
}
}
}
if ($foundArrayy === false) {
$array[$key] = $jsonValue;
}
}
} elseif ($this->isArrayOfType($class)) {
$array[$key] = $this->mapArray(
$jsonValue,
[],
\substr($class, 0, -2)
);
} elseif ($this->isScalarType(\gettype($jsonValue))) {
// Use constructor parameter if we have a class, but only a flat type (i.e. string, int).
if ($jsonValue === null) {
$array[$key] = null;
} elseif ($this->isSimpleType($class)) {
\settype($jsonValue, $class);
$array[$key] = $jsonValue;
} else {
/** @noinspection PhpSillyAssignmentInspection - phpstan helper */
/** @phpstan-var class-string $class */
$class = $class;
$array[$key] = self::createInstance(
$class,
true,
$jsonValue
);
}
} elseif ($this->isScalarType($class)) {
throw new \InvalidArgumentException(
'JSON property "' . ($parent_key ?: '?') . '" is an array of type "' . $class . '" but contained a value of type "' . \gettype($jsonValue) . '"'
);
} elseif (\is_a($class, \ArrayObject::class, true)) {
/** @noinspection PhpSillyAssignmentInspection - phpstan helper */
/** @phpstan-var \ArrayObject $class */
$class = $class;
$array[$key] = $this->mapArray(
$jsonValue,
self::createInstance($class)
);
} else {
/** @noinspection PhpSillyAssignmentInspection - phpstan helper */
/** @phpstan-var class-string $class */
$class = $class;
$array[$key] = $this->map(
$jsonValue,
self::createInstance($class, false, $jsonValue)
);
}
}
return $array;
}
/**
* Convert a type name to a fully namespaced type name.
*
* @param string|null $type Type name (simple type or class name)
* @param string $strNs Base namespace that gets prepended to the type name
*
* @return string|null Fully-qualified type name with namespace
*/
private function getFullNamespace($type, $strNs)
{
if (
$type === null
||
$type === ''
||
$type[0] == '\\'
||
$strNs == ''
) {
return $type;
}
list($first) = \explode('[', $type, 2);
if (
$first === 'mixed'
||
$this->isSimpleType($first)
) {
return $type;
}
//create a full qualified namespace
return '\\' . $strNs . '\\' . $type;
}
/**
* Try to find out if a property exists in a given class.
* Checks property first, falls back to setter method.
*
* @param \ReflectionClass