Created
May 21, 2015 17:09
-
-
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
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 | |
/** | |
* 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