Skip to content

Instantly share code, notes, and snippets.

@jrbasso
Created February 28, 2016 19:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jrbasso/5e43aacc415bedcd4537 to your computer and use it in GitHub Desktop.
Save jrbasso/5e43aacc415bedcd4537 to your computer and use it in GitHub Desktop.
Testing performance with options suggested on zumba/json-serializer#14
<?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