Created
August 8, 2015 23:35
-
-
Save clicman/960e685c450d6442024f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Stom\Api\Util\JsonMapper; | |
use InvalidArgumentException; | |
use ReflectionClass; | |
use ReflectionMethod; | |
/** | |
* Part of JsonMapper | |
* | |
* PHP version 5 | |
* | |
* @category Netresearch | |
* @package JsonMapper | |
* @author Christian Weiske <christian.weiske@netresearch.de> | |
* @license OSL-3.0 http://opensource.org/licenses/osl-3.0 | |
* @link http://www.netresearch.de/ | |
*/ | |
/** | |
* Automatically map JSON structures into objects. | |
* | |
* @category Netresearch | |
* @package JsonMapper | |
* @author Christian Weiske <christian.weiske@netresearch.de> | |
* @license OSL-3.0 http://opensource.org/licenses/osl-3.0 | |
* @link http://www.netresearch.de/ | |
*/ | |
class JsonMapper { | |
/** | |
* PSR-3 compatible logger object | |
* | |
* @link http://www.php-fig.org/psr/psr-3/ | |
* @var object | |
* @see setLogger() | |
*/ | |
protected $logger; | |
/** | |
* Throw an exception when JSON data contain a property | |
* that is not defined in the PHP class | |
* | |
* @var boolean | |
*/ | |
public $bExceptionOnUndefinedProperty = false; | |
/** | |
* Throw an exception if the JSON data miss a property | |
* that is marked with @required in the PHP class | |
* | |
* @var boolean | |
*/ | |
public $bExceptionOnMissingData = false; | |
/** | |
* Runtime cache for inspected classes. This is particularly effective if | |
* mapArray() is called with a large number of objects | |
* | |
* @var array property inspection result cache | |
*/ | |
protected $arInspectedClasses = array(); | |
/** | |
* Map data all data in $json into the given $object instance. | |
* | |
* @param object $json JSON object structure from json_decode() | |
* @param object $object Object to map $json data into | |
* | |
* @return object Mapped object is returned. | |
*/ | |
public function map($json, $object) { | |
// if (!is_object($json)) { | |
// throw new InvalidArgumentException( | |
// 'JsonMapper::map() requires first argument to be an object' | |
// . ', ' . gettype($json) . ' given.' | |
// ); | |
// } | |
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(); | |
$providedProperties = array(); | |
foreach ($json as $key => $jvalue) { | |
$providedProperties[$key] = true; | |
// 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, $isSettable, $type, $setter) = $this->arInspectedClasses[$strClassName][$key]; | |
if (!$hasProperty) { | |
if ($this->bExceptionOnUndefinedProperty) { | |
throw new JsonMapper_Exception( | |
'JSON property "' . $key . '" does not exist' | |
. ' in object of type ' . $strClassName | |
); | |
} | |
$this->log( | |
'info', 'Property {property} does not exist in {class}', array('property' => $key, 'class' => $strClassName) | |
); | |
continue; | |
} | |
if (!$isSettable) { | |
if ($this->bExceptionOnUndefinedProperty) { | |
throw new JsonMapper_Exception( | |
'JSON property "' . $key . '" has no public setter method' | |
. ' in object of type ' . $strClassName | |
); | |
} | |
$this->log( | |
'info', 'Property {property} has no public setter method in {class}', array('property' => $key, 'class' => $strClassName) | |
); | |
continue; | |
} | |
if ($this->isNullable($type)) { | |
if ($jvalue === null) { | |
$this->setProperty($object, $key, null, $setter); | |
continue; | |
} | |
$type = $this->removeNullable($type); | |
} | |
if ($type === null || $type === 'mixed') { | |
//no given type - simply set the json data | |
$this->setProperty($object, $key, $jvalue, $setter); | |
continue; | |
} else if ($this->isObjectOfSameType($type, $jvalue)) { | |
$this->setProperty($object, $key, $jvalue, $setter); | |
continue; | |
} else if ($this->isSimpleType($type)) { | |
settype($jvalue, $type); | |
$this->setProperty($object, $key, $jvalue, $setter); | |
continue; | |
} | |
//FIXME: check if type exists, give detailled error message if not | |
if ($type === '') { | |
throw new JsonMapper_Exception( | |
'Empty type at property "' | |
. $strClassName . '::$' . $key . '"' | |
); | |
} | |
$array = null; | |
$subtype = null; | |
if (substr($type, -2) == '[]') { | |
//array | |
$array = array(); | |
$subtype = substr($type, 0, -2); | |
} else if (substr($type, -1) == ']') { | |
list($proptype, $subtype) = explode('[', substr($type, 0, -1)); | |
if (!$this->isSimpleType($proptype)) { | |
$proptype = $this->getFullNamespace($proptype, $strNs); | |
} | |
$array = new $proptype(); | |
} else if ($type == 'ArrayObject' || is_subclass_of($type, 'ArrayObject') | |
) { | |
$array = new $type(); | |
} | |
if ($array !== null) { | |
if (!$this->isSimpleType($subtype)) { | |
$subtype = $this->getFullNamespace($subtype, $strNs); | |
} | |
if ($jvalue === null) { | |
$child = null; | |
} else { | |
$child = $this->mapArray($jvalue, $array, $subtype); | |
} | |
} else if ($this->isFlatType(gettype($jvalue))) { | |
//use constructor parameter if we have a class | |
// but only a flat type (i.e. string, int) | |
if ($jvalue === null) { | |
$child = null; | |
} else { | |
$type = $this->getFullNamespace($type, $strNs); | |
$child = new $type($jvalue); | |
} | |
} else { | |
$type = $this->getFullNamespace($type, $strNs); | |
$child = new $type(); | |
$this->map($jvalue, $child); | |
} | |
$this->setProperty($object, $key, $child, $setter); | |
} | |
if ($this->bExceptionOnMissingData) { | |
$this->checkMissingData($providedProperties, $rc); | |
} | |
return $object; | |
} | |
/** | |
* Convert a type name to a fully namespaced type name. | |
* | |
* @param string $type Type name (simple type or class name) | |
* @param string $strNs Base namespace that gets prepended to the type name | |
* | |
* @return string Fully-qualified type name with namespace | |
*/ | |
protected function getFullNamespace($type, $strNs) { | |
if ($type !== '' && $type{0} != '\\') { | |
//create a full qualified namespace | |
if ($strNs != '') { | |
$type = '\\' . $strNs . '\\' . $type; | |
} | |
} | |
return $type; | |
} | |
/** | |
* Check required properties exist in json | |
* | |
* @param array $providedProperties array with json properties | |
* @param object $rc Reflection class to check | |
* | |
* @throws JsonMapper_Exception | |
* | |
* @return void | |
*/ | |
protected function checkMissingData($providedProperties, ReflectionClass $rc) { | |
foreach ($rc->getProperties() as $property) { | |
$rprop = $rc->getProperty($property->name); | |
$docblock = $rprop->getDocComment(); | |
$annotations = $this->parseAnnotations($docblock); | |
if (isset($annotations['required']) && !isset($providedProperties[$property->name]) | |
) { | |
throw new JsonMapper_Exception( | |
'Required property "' . $property->name . '" of class ' | |
. $rc->getName() | |
. ' is missing in JSON data' | |
); | |
} | |
} | |
} | |
/** | |
* 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 $class Class name for children objects. | |
* All children will get mapped onto this type. | |
* Supports class names and simple types | |
* like "string". | |
* | |
* @return mixed Mapped $array is returned | |
*/ | |
public function mapArray($json, $array, $class = null) { | |
foreach ($json as $key => $jvalue) { | |
if ($class === null) { | |
$array[$key] = $jvalue; | |
} else if ($this->isFlatType(gettype($jvalue))) { | |
//use constructor parameter if we have a class | |
// but only a flat type (i.e. string, int) | |
if ($jvalue === null) { | |
$array[$key] = null; | |
} else { | |
if ($this->isSimpleType($class)) { | |
$array[$key] = $jvalue; | |
} else { | |
$array[$key] = new $class($jvalue); | |
} | |
} | |
} else { | |
$array[$key] = $this->map($jvalue, new $class()); | |
} | |
} | |
return $array; | |
} | |
/** | |
* Try to find out if a property exists in a given class. | |
* Checks property first, falls back to setter method. | |
* | |
* @param object $rc Reflection class to check | |
* @param string $name Property name | |
* | |
* @return array First value: if the property exists | |
* Second value: is the property settable | |
* Third value: type of the property | |
* Fourth value: the setter to use, otherwise null | |
*/ | |
protected function inspectProperty(ReflectionClass $rc, $name) { | |
//try setter method first | |
$setter = 'set' . str_replace( | |
' ', '', ucwords(str_replace('_', ' ', $name)) | |
); | |
if ($rc->hasMethod($setter)) { | |
$rmeth = $rc->getMethod($setter); | |
if ($rmeth->isPublic()) { | |
$rparams = $rmeth->getParameters(); | |
if (count($rparams) > 0) { | |
$pclass = $rparams[0]->getClass(); | |
if ($pclass !== null) { | |
return array( | |
true, true, '\\' . $pclass->getName(), $rmeth | |
); | |
} | |
} | |
$docblock = $rmeth->getDocComment(); | |
$annotations = $this->parseAnnotations($docblock); | |
if (!isset($annotations['param'][0])) { | |
return array(true, true, null, $rmeth); | |
} | |
list($type) = explode(' ', trim($annotations['param'][0])); | |
return array(true, true, $type, $rmeth); | |
} | |
} | |
//now try to set the property directly | |
if ($rc->hasProperty($name)) { | |
$rprop = $rc->getProperty($name); | |
if ($rprop->isPublic()) { | |
$docblock = $rprop->getDocComment(); | |
$annotations = $this->parseAnnotations($docblock); | |
if (!isset($annotations['var'][0])) { | |
return array(true, true, null, null); | |
} | |
//support "@var type description" | |
list($type) = explode(' ', $annotations['var'][0]); | |
return array(true, true, $type, null); | |
} else { | |
//no setter, private property | |
return array(true, false, null, null); | |
} | |
} | |
//no setter, no property | |
return array(false, false, null, null); | |
} | |
/** | |
* Set a property on a given object to a given value. | |
* | |
* Checks if the setter or the property are public are made before | |
* calling this method. | |
* | |
* @param object $object Object to set property on | |
* @param string $name Property name | |
* @param mixed $value Value of property | |
* @param object $setter The setter to use, null if no setter | |
* should be used | |
* | |
* @return void | |
*/ | |
protected function setProperty( | |
$object, $name, $value, ReflectionMethod $setter = null | |
) { | |
if ($setter === null) { | |
$object->$name = $value; | |
} else { | |
$object->{$setter->getName()}($value); | |
} | |
} | |
/** | |
* Checks if the given type is a "simple type" | |
* | |
* @param string $type type name from gettype() | |
* | |
* @return boolean True if it is a simple PHP type | |
*/ | |
protected function isSimpleType($type) { | |
return $type == 'string' || $type == 'boolean' || $type == 'bool' || $type == 'integer' || $type == 'int' || $type == 'float' || $type == 'array' || $type == 'object'; | |
} | |
/** | |
* Checks if the object is of this type or has this type as one of its parents | |
* | |
* @param string $type class name of type being required | |
* @param mixed $value Some PHP value to be tested | |
* | |
* @return boolean True if $object has type of $type | |
*/ | |
protected function isObjectOfSameType($type, $value) { | |
if (false === is_object($value)) { | |
return false; | |
} | |
return is_a($value, $type); | |
} | |
/** | |
* Checks if the given type is a type that is not nested | |
* (simple type except array and object) | |
* | |
* @param string $type type name from gettype() | |
* | |
* @return boolean True if it is a non-nested PHP type | |
*/ | |
protected function isFlatType($type) { | |
return $type == 'NULL' || $type == 'string' || $type == 'boolean' || $type == 'bool' || $type == 'integer' || $type == 'int' || $type == 'double'; | |
} | |
/** | |
* Checks if the given type is nullable | |
* | |
* @param string $type type name from the phpdoc param | |
* | |
* @return boolean True if it is nullable | |
*/ | |
protected function isNullable($type) { | |
return stripos('|' . $type . '|', '|null|') !== false; | |
} | |
/** | |
* Remove the 'null' section of a type | |
* | |
* @param string $type type name from the phpdoc param | |
* | |
* @return string The new type value | |
*/ | |
protected function removeNullable($type) { | |
return substr( | |
str_ireplace('|null|', '|', '|' . $type . '|'), 1, -1 | |
); | |
} | |
/** | |
* Copied from PHPUnit 3.7.29, Util/Test.php | |
* | |
* @param string $docblock Full method docblock | |
* | |
* @return array | |
*/ | |
protected static function parseAnnotations($docblock) { | |
$annotations = array(); | |
// Strip away the docblock header and footer | |
// to ease parsing of one line annotations | |
$docblock = substr($docblock, 3, -2); | |
$re = '/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m'; | |
if (preg_match_all($re, $docblock, $matches)) { | |
$numMatches = count($matches[0]); | |
for ($i = 0; $i < $numMatches; ++$i) { | |
$annotations[$matches['name'][$i]][] = $matches['value'][$i]; | |
} | |
} | |
return $annotations; | |
} | |
/** | |
* Log a message to the $logger object | |
* | |
* @param string $level Logging level | |
* @param string $message Text to log | |
* @param array $context Additional information | |
* | |
* @return null | |
*/ | |
protected function log($level, $message, array $context = array()) { | |
if ($this->logger) { | |
$this->logger->log($level, $message, $context); | |
} | |
} | |
/** | |
* Sets a logger instance on the object | |
* | |
* @param LoggerInterface $logger PSR-3 compatible logger object | |
* | |
* @return null | |
*/ | |
public function setLogger($logger) { | |
$this->logger = $logger; | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment