Created
February 28, 2016 19:33
-
-
Save jrbasso/5e43aacc415bedcd4537 to your computer and use it in GitHub Desktop.
Testing performance with options suggested on zumba/json-serializer#14
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 | |
class JsonSerializer { | |
const CLASS_IDENTIFIER_KEY = '@type'; | |
const FLOAT_ADAPTER = 'JsonSerializerFloatAdapter'; | |
/** | |
* Storage for object | |
* | |
* Used for recursion | |
* | |
* @var SplObjectStorage | |
*/ | |
protected $objectStorage; | |
/** | |
* Object mapping for recursion | |
* | |
* @var array | |
*/ | |
protected $objectMapping = array(); | |
/** | |
* Object mapping index | |
* | |
* @var integer | |
*/ | |
protected $objectMappingIndex = 0; | |
/** | |
* Support PRESERVE_ZERO_FRACTION json option | |
* | |
* @var boolean | |
*/ | |
protected $preserveZeroFractionSupport; | |
/** | |
* Constructor. | |
*/ | |
public function __construct() { | |
$this->preserveZeroFractionSupport = defined('JSON_PRESERVE_ZERO_FRACTION'); | |
} | |
/** | |
* Serialize the value in JSON | |
* | |
* @param mixed $value | |
* @param integer $options | |
* @return string JSON encoded | |
* @throws Exception | |
*/ | |
public function serialize($value, $options = -1) { | |
$this->reset(); | |
if ($options < 0) { | |
$options = $this->calculateEncodeOptions(); | |
} | |
$encoded = json_encode($this->serializeData($value), $options); | |
return $this->processEncodedValue($encoded); | |
} | |
/** | |
* Calculate encoding options | |
* | |
* @return integer | |
*/ | |
protected function calculateEncodeOptions() { | |
$options = JSON_UNESCAPED_UNICODE; | |
if ($this->preserveZeroFractionSupport) { | |
$options |= JSON_PRESERVE_ZERO_FRACTION; | |
} | |
return $options; | |
} | |
/** | |
* Execute post-encoding actions | |
* | |
* @param string $encoded | |
* @return string | |
*/ | |
protected function processEncodedValue($encoded) { | |
if (!$this->preserveZeroFractionSupport) { | |
$encoded = preg_replace('/"' . static::FLOAT_ADAPTER . '\((.*?)\)"/', '\1', $encoded); | |
} | |
return $encoded; | |
} | |
/** | |
* Unserialize the value from JSON | |
* | |
* @param string $value | |
* @return mixed | |
*/ | |
public function unserialize($value) { | |
$this->reset(); | |
return $this->unserializeData(json_decode($value, true)); | |
} | |
/** | |
* Parse the data to be json encoded | |
* | |
* @param mixed $value | |
* @return mixed | |
* @throws Exception | |
*/ | |
protected function serializeData($value) { | |
if (is_scalar($value) || $value === null) { | |
if (!$this->preserveZeroFractionSupport && is_float($value) && strpos((string)$value, '.') === false) { | |
// Because the PHP bug #50224, the float numbers with no | |
// precision numbers are converted to integers when encoded | |
$value = static::FLOAT_ADAPTER . '(' . $value . '.0)'; | |
} | |
return $value; | |
} | |
if (is_resource($value)) { | |
throw new Exception('Resource is not supported in JsonSerializer'); | |
} | |
if (is_array($value)) { | |
return array_map(array($this, __FUNCTION__), $value); | |
} | |
if ($value instanceof \Closure) { | |
throw new Exception('Closures are not supported in JsonSerializer'); | |
} | |
return $this->serializeObject($value); | |
} | |
/** | |
* Extract the data from an object | |
* | |
* @param object $value | |
* @return array | |
*/ | |
protected function serializeObject($value) { | |
$ref = new ReflectionClass($value); | |
if ($this->objectStorage->contains($value)) { | |
return array(static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]); | |
} | |
$this->objectStorage->attach($value, $this->objectMappingIndex++); | |
$paramsToSerialize = $this->getObjectProperties($ref, $value); | |
$data = array(static::CLASS_IDENTIFIER_KEY => $ref->getName()); | |
$data += array_map(array($this, 'serializeData'), $this->extractObjectData($value, $ref, $paramsToSerialize)); | |
return $data; | |
} | |
/** | |
* Return the list of properties to be serialized | |
* | |
* @param ReflectionClass $ref | |
* @param object $value | |
* @return array | |
*/ | |
protected function getObjectProperties($ref, $value) { | |
if (method_exists($value, '__sleep')) { | |
return $value->__sleep(); | |
} | |
$props = array(); | |
foreach ($ref->getProperties() as $prop) { | |
$props[] = $prop->getName(); | |
} | |
return array_unique(array_merge($props, array_keys(get_object_vars($value)))); | |
} | |
/** | |
* Extract the object data | |
* | |
* @param object $value | |
* @param ReflectionClass $ref | |
* @param array $properties | |
* @return array | |
*/ | |
protected function extractObjectData($value, $ref, $properties) { | |
$data = array(); | |
foreach ($properties as $property) { | |
try { | |
$propRef = $ref->getProperty($property); | |
$propRef->setAccessible(true); | |
$data[$property] = $propRef->getValue($value); | |
} catch (ReflectionException $e) { | |
$data[$property] = $value->$property; | |
} | |
} | |
return $data; | |
} | |
/** | |
* Parse the json decode to convert to objects again | |
* | |
* @param mixed $value | |
* @return mixed | |
*/ | |
protected function unserializeData($value) { | |
if (is_scalar($value) || $value === null) { | |
return $value; | |
} | |
return isset($value[static::CLASS_IDENTIFIER_KEY]) ? | |
$this->unserializeObject($value) : | |
array_map(array($this, __FUNCTION__), $value); | |
} | |
/** | |
* Convert the serialized array into an object | |
* | |
* @param aray $value | |
* @return object | |
* @throws Exception | |
*/ | |
protected function unserializeObject($value) { | |
$className = $value[static::CLASS_IDENTIFIER_KEY]; | |
unset($value[static::CLASS_IDENTIFIER_KEY]); | |
if ($className[0] === '@') { | |
$index = substr($className, 1); | |
return $this->objectMapping[$index]; | |
} | |
if (!class_exists($className)) { | |
throw new Exception('Unable to find class ' . $className); | |
} | |
if ($className === 'DateTime') { | |
$obj = $this->restoreUsingUnserialize($className, $value); | |
$this->objectMapping[$this->objectMappingIndex++] = $obj; | |
return $obj; | |
} | |
$ref = new ReflectionClass($className); | |
$obj = $ref->newInstanceWithoutConstructor(); | |
$this->objectMapping[$this->objectMappingIndex++] = $obj; | |
foreach ($value as $property => $propertyValue) { | |
try { | |
$propRef = $ref->getProperty($property); | |
$propRef->setAccessible(true); | |
$propRef->setValue($obj, $this->unserializeData($propertyValue)); | |
} catch (ReflectionException $e) { | |
$obj->$property = $this->unserializeData($propertyValue); | |
} | |
} | |
if (method_exists($obj, '__wakeup')) { | |
$obj->__wakeup(); | |
} | |
return $obj; | |
} | |
protected function restoreUsingUnserialize($className, $attributes) { | |
$obj = (object)$attributes; | |
$serialized = preg_replace('|^O:\d+:"\w+":|', 'O:' . strlen($className) . ':"' . $className . '":', serialize($obj)); | |
return unserialize($serialized); | |
} | |
/** | |
* Reset variables | |
* | |
* @return void | |
*/ | |
protected function reset() { | |
$this->objectStorage = new SplObjectStorage(); | |
$this->objectMapping = array(); | |
$this->objectMappingIndex = 0; | |
} | |
} | |
$data = (object)[ | |
'a' => 'b', | |
'c' => 'd', | |
'e' => (object)[ | |
'e1' => 1, | |
'e2' => 2, | |
'e3' => 3 | |
], | |
'f' => [true, false], | |
'g' => ['lat' => 0.123, 'lng' => 12.34], | |
'h' => 'Some test with "quotes" and sl/as\\hs' | |
]; | |
$iterations = 10000; | |
$serializer = new JsonSerializer(); | |
// Just to warm any opcache or internal memory necessary for it | |
$i = $iterations; | |
$serializer->serialize($data); | |
// Testing the original code | |
$m = microtime(true); | |
for ($i = $iterations; $i > 0; $i--) { | |
$serializer->serialize($data); | |
} | |
echo "Original code: ", microtime(true) - $m, "\n"; | |
// Testing using suggested constant | |
$m = microtime(true); | |
for ($i = $iterations; $i > 0; $i--) { | |
$serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); | |
} | |
echo "Suggested options: ", microtime(true) - $m, "\n"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment