Skip to content

Instantly share code, notes, and snippets.

@RobThree
Created May 21, 2015 17:09
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 RobThree/1c4cce8a88c1b20c7589 to your computer and use it in GitHub Desktop.
Save RobThree/1c4cce8a88c1b20c7589 to your computer and use it in GitHub Desktop.
Class used to merge objects based on a specific syntax specified as strings
<?php
/**
* Dynamically merges objects; it allows for string-values to be 'expanded' into actual object values following a simple
* syntax: $objectname.property or $objectname.property.property.property[foo] or $objectname[1]
*
* Example:
*
* $source = array('customer' => $customerobject, 'order' => $orderobject);
* $dest = json_decode('{ "myreference": "abc-123", "customer_id": "{$customer.id}", "order_id": "{$order.id}" }');
*
* //$dest is an object:
* object(stdClass) {
* ["myreference"]=>
* string "abc-123"
* ["customer_id"]=>
* string "{$customer.id}"
* ["order_id"]=>
* string "{$order.id}"
* }
*
* $m = new ObjectMerger();
* $m->MergeObject($source, $dest);
*
* //$dest is now merged:
* object(stdClass) {
* ["myreference"]=>
* string "abc-123"
* ["customer_id"]=>
* int 378871
* ["order_id"]=>
* int 1894
* }
*/
class ObjectMerger
{
private $defaultitemname; // Default object name
private $allowprivateaccess; // Do we allow access to private members of the object merging FROM?
/**
* Initializes a new instance of an ObjectMerger
* @param string $defaultitemname
* @param bool $allowprivateaccess
* @throws InvalidArgumentException
*/
public function __construct($defaultitemname = 'item', $allowprivateaccess = false)
{
// Sanity checks
if (!is_bool($allowprivateaccess))
throw new InvalidArgumentException('Allowprivateaccess must be bool');
if (preg_match('/^[a-z0-9_]+$/i', $defaultitemname) !== 1)
throw new InvalidArgumentException('Invalid defaultitemname');
// Initialize our private members
$this->defaultitemname = $defaultitemname;
$this->allowprivateaccess = $allowprivateaccess;
}
/**
* Merges the source object(s) to the destination object
* @param mixed $source An object or associative array containing objects that provide the values that will
* be merged to the destinationobject
* @param mixed $destination The object to merge to; any string values (note: not keys!) containing references
* to $item.foo.bar will be replaced with actual values from $item->foo->bar
* @param mixed $undefvalue The value to return when a property/value is not found in the source object
* @throws InceptionException Thrown when the destination object contains the source object
* @return mixed
*/
public function MergeObject($source, $destination, $undefvalue = null)
{
// The object we'll be retrieving values FROM should be an array (e.g. "array("item"=> $someobject, "user"=> $user)")
// If it isn't we'll assume the passed $sourceobject is the ONLY item and stuff it in an array with the defaultitemname as key
if (!is_array($source))
$source = array($this->defaultitemname => $source);
// Make sure the object doesn't equal (and because we're recursive: doesn't **contain**) the sourceobject
if ($destination === $source)
throw new InceptionException('Destination object contains source object');
// If the object to merge TO is null we're done pretty quick
if ($destination === null)
return null;
// If the object to merge TO is an actual object; walk it's properties and call ourselves recursively
if (is_object($destination))
{
foreach ($destination as $k => $v)
$destination->$k = $this->MergeObject($source, $v, $undefvalue);
} elseif (is_array($destination)) { // If the object to merge TO is an array, walk each key/value pair and call ourselves recursively
foreach ($destination as $k => $v)
$destination[$k] = $this->MergeObject($source, $v, $undefvalue);
} else {
// If the object is a string and matches the {$name.foo.bar}-syntax
if (is_string($destination) && preg_match('/^\{\$([a-z0-9_]+)\.?([a-z0-9_\[\]\.]*?)\}$/i', $destination, $matches) === 1) {
// Figure out the desired hierarchy
$identifiers = explode('.', $matches[2]);
// Get the object to retrieve the value from
$valueobject = $this->GetValue($source, $matches[1]);
// If no hierarchy: return the valueobject
if (strlen(trim($identifiers[0]))==0)
return $valueobject;
// Walk the hierarchy on the valueobject, as deep as the rabbithole goes
$i = 0;
$v = $this->GetValue($valueobject, $identifiers[$i++], $undefvalue);
while ($v !== null && $i < sizeof($identifiers))
$v = $this->GetValue($v, $identifiers[$i++], $undefvalue);
// We should have a value by now (or null if we couldn't walk the hierarchy all the way)
return $v;
}
}
// Simply return the original if all above didn't match
return $destination;
}
/**
* Gets a value from an (associative) array or object property
* @param mixed $object The object to get a value from
* @param mixed $name The name of the value (or property) to get
* @param mixed $defaultvalue The default value to return when the value is not found
* @return mixed
*/
private function GetValue($object, $name, $defaultvalue = null) {
// If the value represents an array notation (e.g. "data[foo]" or "bar[0]") we try to extract the
// actual value from the array/property
if (preg_match('/^(.*)\[(.*)\]$/', $name, $matches) === 1)
return $this->GetValue($this->GetValue($object, $matches[1]), $matches[2]);
// Try to extract a value from an array (if the key/index exists)
if (is_array($object))
return isset($object[$name]) ? $object[$name] : $defaultvalue;
// Try to extract a value from a property (if the property exists)
if (is_object($object))
return $this->GetPropertyValue($object, $name, $defaultvalue);
// Nothing worked; return default value
return $defaultvalue;
}
/**
* Gets the value of an object's specified property; depending on the ObjectMerger's allowprivate setting also
* returns values from private properties
* @param mixed $object The object to get the value from
* @param mixed $property The property to get the value from
* @param mixed $defaultvalue The default value to return when the property isn't found and/or not accessible
* @return mixed
*/
private function GetPropertyValue($object, $property, $defaultvalue = null) {
$r = new ReflectionClass($object);
// Check to see if we have the desired property
if ($r->hasProperty($property)) {
// Get a reference to the property
$pi = $r->getProperty($property);
// If allowprivateaccess then we make sure the property is accessible
if ($this->allowprivateaccess)
$pi->setAccessible(true);
// If the desired property is public OR we're allowed to access privates we return it's value
if ($pi->isPublic() || $this->allowprivateaccess)
return $pi->getValue($object);
}
// Return default value
return $defaultvalue;
}
}
/**
* Thrown when an 'inception' (e.g. object containing instance of itself) occurs.
*/
class InceptionException extends Exception
{
function __construct($message = "", $code = 0, $previous = NULL)
{
parent::__construct($message, $code, $previous);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment