Skip to content

Instantly share code, notes, and snippets.

@billschaller
Last active August 29, 2015 14:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save billschaller/69383ea7fd341a97b621 to your computer and use it in GitHub Desktop.
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.
<?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;
}
}
<?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