Last active
August 29, 2015 14:03
-
-
Save billschaller/69383ea7fd341a97b621 to your computer and use it in GitHub Desktop.
How to json_encode Doctrine (or other) model objects nicely without recursion issues.
This file contains hidden or 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 JsonHelper | |
* | |
* Handles json_encoding Model objects in a way that prevents recursion issues. Stores hashes of | |
* "seen" objects, and if an object has already been seen, the JsonSerializableTrait unsets the key | |
* of the duplicate object. | |
* | |
*/ | |
class JsonHelper | |
{ | |
private static $toVisit = array(); | |
private static $visited = array(); | |
private static $visiting = null; | |
private static $level = 0; | |
/** | |
* json_encode | |
* | |
* Helper function to handle initialization of JsonHelper structures. Use this instead of | |
* the standard json_encode. If serialization fails, this func will return the error message | |
* instead of false. | |
* | |
* @param mixed $data | |
* @param int $options | |
* @param int $depth | |
* @return string | |
*/ | |
public static function json_encode($data, $options = 0, $depth = 512) | |
{ | |
self::$toVisit = self::$visited = self::$visiting = array(); | |
$json = json_encode($data, $options, $depth); | |
if ($json === false) { | |
$json = json_last_error_msg(); | |
} | |
return $json; | |
} | |
/** | |
* beginVisit | |
* | |
* Method to signal that preparation of an object for serialization is beginning. | |
* This method is called at the beginning of the jsonSerialize method in JsonSerializableTrait. | |
* | |
* @param object $object object being serialized | |
*/ | |
public static function beginVisit($object) | |
{ | |
$objectHash = spl_object_hash($object); | |
if (!isset(self::$toVisit[self::$level][$objectHash])) { | |
if (self::$level == 0) { | |
self::$visiting = spl_object_hash($object); | |
unset(self::$toVisit[self::$level][$objectHash]); | |
} else { | |
trigger_error("Visiting unscheduled object in JsonHelper::beginVisit", E_USER_WARNING); | |
} | |
} else { | |
if (self::$toVisit[self::$level][$objectHash] > 1) { | |
self::$toVisit[self::$level][$objectHash]--; | |
} else { | |
unset(self::$toVisit[self::$level][$objectHash]); | |
} | |
self::$visiting = $objectHash; | |
} | |
} | |
/** | |
* endVisit | |
* | |
* Method to signal that preparation of an object for serialization is complete. | |
* This is called at the end of the jsonSerialize method in JsonSerializableTrait. | |
* | |
* @param object $object object being serialized | |
*/ | |
public static function endVisit($object) | |
{ | |
$objectHash = spl_object_hash($object); | |
if (!isset(self::$visited[self::$level][$objectHash])) { | |
self::$visited[self::$level][$objectHash] = 1; | |
} else { | |
self::$visited[self::$level][$objectHash]++; | |
} | |
if (!empty(self::$toVisit[self::$level + 1])) { | |
// there are things on a deeper level to visit | |
self::$level++; | |
} else { | |
// There is nothing on a deeper level to visit. | |
while (empty(self::$toVisit[self::$level]) && self::$level > 0) { | |
// Nothing left on this level to visit, this is the last item | |
self::$visited[self::$level] = array(); | |
self::$level--; | |
} | |
} | |
self::$visiting = null; | |
} | |
/** | |
* toVisit | |
* | |
* This method signals that an object will be visited in the next depth level of the tree | |
* after the object currently being prepared. This method is called for all objects referenced by | |
* the object currently being prepared for serialization. JsonHelper will expect to see beginVisit | |
* and endVisit called for all objects referenced as parameters to this function. | |
* | |
* @param object $object object to be visited | |
*/ | |
public static function toVisit($object) | |
{ | |
$objectHash = spl_object_hash($object); | |
if (!is_array(self::$toVisit[self::$level+1])) { | |
self::$toVisit[self::$level+1] = array(); | |
} | |
if (!isset(self::$toVisit[self::$level+1][$objectHash])) { | |
self::$toVisit[self::$level+1][$objectHash] = 1; | |
} else { | |
self::$toVisit[self::$level+1][$objectHash]++; | |
} | |
} | |
/** | |
* checkVisited | |
* | |
* This method returns true if the object specified has already been visited once in the path | |
* from root to leaf of the object tree being serialized. If this method returns true, the object | |
* referenced is a circular reference, and JsonSerializableTrait will remove it from the set of objects | |
* being prepared for serialization. | |
* | |
* @param object $object object to check | |
*/ | |
public static function checkVisited($object) | |
{ | |
$objectHash = spl_object_hash($object); | |
for ($i = 0; $i < self::$level; $i++) { | |
if (isset(self::$visited[$i][$objectHash])) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} |
This file contains hidden or 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 | |
use Doctrine\Common\Collections\Collection; | |
use Doctrine\ORM\PersistentCollection; | |
use Doctrine\ORM\Proxy\Proxy; | |
trait JsonSerializableTrait { | |
protected static $_jsonIgnoredProperties = array( | |
"create_date", | |
"update_date", | |
"create_user", | |
"update_user" | |
); | |
/** | |
* getJsonSerializerIgnoredProperties | |
* | |
* Gets a list of properties to ignore during serialization. This trait has defaults. Implementing | |
* classes can add additional ignored properties by setting a property, jsonIgnoredProperties on the object. | |
* | |
* @return array | |
*/ | |
private function getJsonSerializerIgnoredProperties() | |
{ | |
// Merge base ignored properties with the ignored properties from the implementing class, if they exist. | |
$ignoredProperties = self::$_jsonIgnoredProperties; | |
if (isset($this->jsonIgnoredProperties)) { | |
$ignoredProperties = array_merge($ignoredProperties, $this->jsonIgnoredProperties); | |
} | |
return array_combine($ignoredProperties, array_fill(0, count($ignoredProperties), true)); | |
} | |
/** | |
* jsonSerialize | |
* | |
* This method prepares the class members for serialization, and eliminates circular references. | |
* | |
* @return array|null|\stdClass | |
*/ | |
public function jsonSerialize() | |
{ | |
// Signal the JsonHelper that the visit is beginning. | |
JsonHelper::beginVisit($this); | |
// handles uninitialized doctrine proxies. | |
if($this instanceof Proxy && !$this->__isInitialized()) { | |
// If proxies are not initialized before calling json_encode on an entity, they are returned as an empty object. | |
JsonHelper::endVisit($this); | |
return new \stdClass; | |
} | |
// Get all vars in this instance | |
$objectVars = get_object_vars($this); | |
// Get only the variables that belong to the class being serialized | |
$classVars = get_class_vars(__CLASS__); | |
//Filter out ignored properties and misc properties from proxy classes | |
$returnVars = array_diff_key( | |
array_intersect_key($objectVars, $classVars), | |
$this->getJsonSerializerIgnoredProperties() | |
); | |
// Don't return PersistentCollection objects that aren't initialized. | |
foreach ($returnVars as $key => $value) { | |
// Don't consider non-objects. Make sure you use collection classes... | |
if (!is_object($value)) { | |
continue; | |
} | |
/** | |
* If the object being considered has already been visited in the path from root to leaf of this | |
* object tree, we don't want to try to reserialize it. json_encode would detect that, return false, | |
* and throw a "recursion detected" error. | |
*/ | |
if (JsonHelper::checkVisited($value)) { | |
unset($returnVars[$key]); | |
continue; | |
} | |
if ($value instanceof PersistentCollection && !$value->isInitialized()) { | |
/** | |
* We don't want to materialize uninitialized PersistentCollection objects, as that would trigger | |
* lazy loading of the entire collection, and that would be bad. Do fetch joins to get everything | |
* you need serialized. | |
*/ | |
$returnVars[$key] = array(); | |
} elseif ($value instanceof Collection) { | |
// Initialized PersistentCollections are converted to arrays before marking the contents as toVisit. | |
$returnVars[$key] = $value->toArray(); | |
foreach ($returnVars[$key] as $idx => $val) { | |
if (!($val instanceof \JsonSerializable)) { | |
// Everything must be JsonSerializable to be considered. | |
unset($returnVars[$key][$idx]); | |
} else { | |
JsonHelper::toVisit($val); | |
} | |
} | |
} elseif (!($value instanceof \JsonSerializable)) { | |
// Everything must be JsonSerializable to be considered. | |
unset($returnVars[$key]); | |
} else { | |
// Mark as toVisit | |
JsonHelper::toVisit($value); | |
} | |
} | |
// Signal the end of the visit for this object. | |
JsonHelper::endVisit($this); | |
return $returnVars; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment