diff --git a/core/model/HasManyList.php b/core/model/HasManyList.php new file mode 100644 index 000000000..194d49443 --- /dev/null +++ b/core/model/HasManyList.php @@ -0,0 +1,76 @@ + value filters that define which records + * in the $dataClass table actually belong to this relationship. + */ + function __construct($dataClass, $foreignKey) { + parent::__construct($dataClass); + $this->foreignKey = $foreignKey; + } + + protected function foreignIDFilter() { + // Apply relation filter + if(is_array($this->foreignID)) { + return "\"$this->foreignKey\" IN ('" . + implode(', ', array_map('Convert::raw2sql', $this->foreignID)) . "')"; + } else if($this->foreignID){ + return "\"$this->foreignKey\" = '" . + Convert::raw2sql($this->foreignID) . "'"; + } + } + + /** + * Adds the item to this relation. + * It does so by setting the relationFilters. + * @param $item The DataObject to be added, or its ID + */ + function add($item) { + if(is_numeric($item)) $item = DataObject::get_by_id($this->dataClass, $item); + else if(!($item instanceof $this->dataClass)) user_eror("HasManyList::add() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + + // Validate foreignID + if(!$this->foreignID) { + user_error("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING); + return; + } + if(is_array($this->foreignID)) { + user_error("ManyManyList::add() can't be called on a list linked to mulitple foreign IDs", E_USER_WARNING); + return; + } + + $fk = $this->foreignKey; + $item->$fk = $this->foreignID; + + $item->write(); + } + + /** + * Remove an item from this relation. + * Doesn't actually remove the item, it just clears the foreign key value. + * @param $item The DataObject to be removed, or its ID + * @todo Maybe we should delete the object instead? + */ + function remove($item) { + if(is_numeric($item)) $item = DataObject::get_by_id($this->dataClass, $item); + else if(!($item instanceof $this->dataClass)) user_eror("HasManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + + $fk = $this->foreignKey; + $item->$fk = null; + + $item->write(); + } + +} diff --git a/core/model/ManyManyList.php b/core/model/ManyManyList.php new file mode 100644 index 000000000..5814644d2 --- /dev/null +++ b/core/model/ManyManyList.php @@ -0,0 +1,166 @@ + fieldtype of extra fields on the join table. + */ + function __construct($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()) { + parent::__construct($dataClass); + $this->joinTable = $joinTable; + $this->localKey = $localKey; + $this->foreignKey = $foreignKey; + $this->extraFields = $extraFields; + + $baseClass = ClassInfo::baseDataClass($dataClass); + + // Join to the many-many join table + $this->dataQuery->innerJoin($joinTable, "\"$this->localKey\" = \"$baseClass\".\"ID\""); + + // Query the extra fields from the join table + if($extraFields) $this->dataQuery->selectFromTable($joinTable, array_keys($extraFields)); + } + + /** + * Return a filter expression for the foreign ID. + */ + protected function foreignIDFilter() { + // Apply relation filter + if(is_array($this->foreignID)) { + return "\"$this->joinTable\".\"$this->foreignKey\" IN ('" . + implode(', ', array_map('Convert::raw2sql', $this->foreignID)) . "')"; + } else if($this->foreignID){ + return "\"$this->joinTable\".\"$this->foreignKey\" = '" . + Convert::raw2sql($this->foreignID) . "'"; + } + } + + /** + * Add an item to this many_many relationship + * Does so by adding an entry to the joinTable. + * @param $extraFields A map of additional columns to insert into the joinTable + */ + function add($item, $extraFields = null) { + if(is_numeric($item)) $itemID = $item; + else if($item instanceof $this->dataClass) $itemID = $item->ID; + else throw new InvalidArgumentException("ManyManyList::add() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + + // Validate foreignID + if(!$this->foreignID) { + throw new Exception("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING); + } + if(is_array($this->foreignID)) { + throw new Exception("ManyManyList::add() can't be called on a list linked to mulitple foreign IDs", E_USER_WARNING); + } + + // Delete old entries, to prevent duplication + $this->remove($itemID); + + // Insert new entry + $manipulation = array(); + $manipulation[$this->joinTable]['command'] = 'insert'; + + if($extraFields) foreach($extraFields as $k => $v) { + $manipulation[$this->joinTable]['fields'][$k] = "'" . Convert::raw2sql($v) . "'"; + } + + $manipulation[$this->joinTable]['fields'][$this->localKey] = $itemID; + $manipulation[$this->joinTable]['fields'][$this->foreignKey] = $this->foreignID; + + DB::manipulate($manipulation); + } + + /** + * Remove the given item from this list. + * Note that for a ManyManyList, the item is never actually deleted, only the join table is affected + * @param $item The data object or its ID + */ + function remove($item) { + if(is_numeric($item)) $itemID = $item; + else if($item instanceof $this->dataClass) $itemID = $item->ID; + else user_eror("ManyManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + + $query = new SQLQuery("*", array($this->joinTable)); + $query->delete = true; + + if($filter = $this->foreignIDFilter()) { + $query->where($filter); + } else { + user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING); + } + + $query->where("\"$this->localKey\" = {$itemID}"); + $query->execute(); + } + + /** + * Remove all items from this many-many join that match the given filter + * @deprecated this is experimental and will change. Don't use it in your projects. + */ + function removeByFilter($filter) { + $query = new SQLQuery("*", array($this->joinTable)); + $query->delete = true; + $query->where($filter); + $query->execute(); + } + + /** + * Find the extra field data for a single row of the relationship + * join table, given the known child ID. + * + * @todo Add tests for this / refactor it / something + * + * @param string $componentName The name of the component + * @param int $childID The ID of the child for the relationship + * @return array Map of fieldName => fieldValue + */ + function getExtraData($componentName, $childID) { + $ownerObj = $this->ownerObj; + $parentField = $this->ownerClass . 'ID'; + $childField = ($this->childClass == $this->ownerClass) ? 'ChildID' : ($this->childClass . 'ID'); + $result = array(); + + if(!isset($componentName)) { + user_error('ComponentSet::getExtraData() passed a NULL component name', E_USER_ERROR); + } + + if(!is_numeric($childID)) { + user_error('ComponentSet::getExtraData() passed a non-numeric child ID', E_USER_ERROR); + } + + // @todo Optimize into a single query instead of one per extra field + if($this->extraFields) { + foreach($this->extraFields as $fieldName => $dbFieldSpec) { + $query = DB::query("SELECT \"$fieldName\" FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$childID}"); + $value = $query->value(); + $result[$fieldName] = $value; + } + } + + return $result; + } +} diff --git a/core/model/RelationList.php b/core/model/RelationList.php new file mode 100644 index 000000000..05321f2f1 --- /dev/null +++ b/core/model/RelationList.php @@ -0,0 +1,34 @@ +foreignID = $id; + + $this->dataQuery->filter($this->foreignIDFilter()); + } + + /** + * Returns this ManyMany relationship linked to the given foreign ID. + * @param $id An ID or an array of IDs. + */ + function forForeignID($id) { + $this->setForeignID($id); + return $this; + } + + abstract protected function foreignIDFilter(); +} \ No newline at end of file diff --git a/model/ComponentSet.php b/model/ComponentSet.php index 01684066d..800878f34 100755 --- a/model/ComponentSet.php +++ b/model/ComponentSet.php @@ -1,307 +1,12 @@ type = $type; - $this->ownerObj = $ownerObj; - $this->ownerClass = $ownerClass ? $ownerClass : $ownerObj->class; - $this->tableName = $tableName; - $this->childClass = $childClass; - $this->joinField = $joinField; - } - - /** - * Get the ComponentSet specific information - * - * Returns an array on the format array( - * 'type' => , - * 'ownerObj' => , - * 'ownerClass' => , - * 'tableName' => , - * 'childClass' => , - * 'joinField' => |null ); - * - * @return array - */ - public function getComponentInfo() { - return array( - 'type' => $this->type, - 'ownerObj' => $this->ownerObj, - 'ownerClass' => $this->ownerClass, - 'tableName' => $this->tableName, - 'childClass' => $this->childClass, - 'joinField' => $this->joinField - ); - } - - /** - * Get an array of all the IDs in this component set, where the keys are the same as the - * values. - * @return array - */ - function getIdList() { - $list = array(); - foreach($this->items as $item) { - $list[$item->ID] = $item->ID; - } - return $list; - } - - /** - * Add an item to this set. - * @param DataObject|int|string $item Item to add, either as a DataObject or as the ID. - * @param array $extraFields A map of extra fields to add. - */ - function add($item, $extraFields = null) { - if(!isset($item)) { - user_error("ComponentSet::add() Not passed an object or ID", E_USER_ERROR); - } - - if(is_object($item)) { - if(!is_a($item, $this->childClass)) { - user_error("ComponentSet::add() Tried to add an '{$item->class}' object, but a '{$this->childClass}' object expected", E_USER_ERROR); - } - } else { - if(!$this->childClass) { - user_error("ComponentSet::add() \$this->childClass not set", E_USER_ERROR); - } - - $item = DataObject::get_by_id($this->childClass, $item); - if(!$item) return; - } - - // If we've already got a database object, then update the database - if($this->ownerObj->ID && is_numeric($this->ownerObj->ID)) { - $this->loadChildIntoDatabase($item, $extraFields); - } - - // In either case, add something to $this->items - $this->items[] = $item; - } - - /** - * Method to save many-many join data into the database for the given $item. - * Used by add() and write(). - * @param DataObject|string|int The item to save, as either a DataObject or the ID. - * @param array $extraFields Map of extra fields. - */ - protected function loadChildIntoDatabase($item, $extraFields = null) { - if($this->type == '1-to-many') { - $child = DataObject::get_by_id($this->childClass,$item->ID); - if (!$child) $child = $item; - $joinField = $this->joinField; - $child->$joinField = $this->ownerObj->ID; - $child->write(); - - } else { - $parentField = $this->ownerClass . 'ID'; - $childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID'); - - DB::query( "DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$item->ID}" ); - - $extraKeys = $extraValues = ''; - if($extraFields) foreach($extraFields as $k => $v) { - $extraKeys .= ", \"$k\""; - $extraValues .= ", '" . DB::getConn()->addslashes($v) . "'"; - } - - DB::query("INSERT INTO \"$this->tableName\" (\"$parentField\",\"$childField\" $extraKeys) VALUES ({$this->ownerObj->ID}, {$item->ID} $extraValues)"); - } - } - - /** - * Add a number of items to the component set. - * @param array $items Items to add, as either DataObjects or IDs. - */ - function addMany($items) { - foreach($items as $item) { - $this->add($item); - } - } - - /** - * Sets the ComponentSet to be the given ID list. - * Records will be added and deleted as appropriate. - * @param array $idList List of IDs. - */ - function setByIDList($idList) { - $has = array(); - // Index current data - if($this->items) foreach($this->items as $item) { - $has[$item->ID] = true; - } - - // Keep track of items to delete - $itemsToDelete = $has; - - // add items in the list - // $id is the database ID of the record - if($idList) foreach($idList as $id) { - $itemsToDelete[$id] = false; - if($id && !isset($has[$id])) $this->add($id); - } - - // delete items not in the list - $removeList = array(); - foreach($itemsToDelete as $id => $actuallyDelete) { - if($actuallyDelete) $removeList[] = $id; - } - $this->removeMany($removeList); - } - - /** - * Remove an item from this set. - * - * @param DataObject|string|int $item Item to remove, either as a DataObject or as the ID. - */ - function remove($item) { - if(is_object($item)) { - if(!is_a($item, $this->childClass)) { - user_error("ComponentSet::remove() Tried to remove an '{$item->class}' object, but a '{$this->childClass}' object expected", E_USER_ERROR); - } - } else { - $item = DataObject::get_by_id($this->childClass, $item); - } - - // Manipulate the database, if it's in there - if($this->ownerObj->ID && is_numeric($this->ownerObj->ID)) { - if($this->type == '1-to-many') { - $child = DataObject::get_by_id($this->childClass,$item->ID); - $joinField = $this->joinField; - if($child->$joinField == $this->ownerObj->ID) { - $child->$joinField = null; - $child->write(); - } - - } else { - $parentField = $this->ownerClass . 'ID'; - $childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID'); - DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$item->ID}"); - } - } - - // Manipulate the in-memory array of items - if($this->items) foreach($this->items as $i => $candidateItem) { - if($candidateItem->ID == $item->ID) { - unset($this->items[$i]); - break; - } - } - } - - /** - * Remove many items from this set. - * @param array $itemList The items to remove, as a numerical array with IDs or as a DataObjectSet - */ - function removeMany($itemList) { - if(!count($itemList)) return false; - - if($this->type == '1-to-many') { - foreach($itemList as $item) $this->remove($item); - } else { - $itemCSV = implode(", ", $itemList); - $parentField = $this->ownerClass . 'ID'; - $childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID'); - DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" IN ($itemCSV)"); - } + user_error("ComponentSet is deprecated; use HasManyList or ManyManyList", E_USER_WARNING); } - /** - * Remove all items in this set. - */ - function removeAll() { - if(!empty($this->tableName)) { - $parentField = $this->ownerClass . 'ID'; - DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID}"); - } else { - foreach($this->items as $item) { - $this->remove($item); - } - } - } - - /** - * Write this set to the database. - * Called by DataObject::write(). - * @param boolean $firstWrite This should be set to true if it the first time the set is being written. - */ - function write($firstWrite = false) { - if($firstWrite) { - foreach($this->items as $item) { - $this->loadChildIntoDatabase($item); - } - } - } - - /** - * Returns information about this set in HTML format for debugging. - * - * @return string - */ - function debug() { - $size = count($this->items); - - $output = <<ComponentSet -
    -
  • Type: {$this->type}
  • -
  • Size: $size
  • -
- -OUT; - - return $output; - } } ?> diff --git a/model/DataObject.php b/model/DataObject.php index 5e22b1aec..37ef4c05e 100755 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -1144,7 +1144,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $this->extend('onAfterSkippedWrite'); } - // Write ComponentSets as necessary + // Write relations as necessary if($writeComponents) { $this->writeComponents(true); } @@ -1307,10 +1307,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity protected $componentCache; /** - * Returns a one-to-many component, as a ComponentSet. - * The return value will be cached on this object instance, - * but only when no related objects are found (to avoid unnecessary empty checks in the database). - * If related objects exist, no caching is applied. + * Returns a one-to-many relation as a HasManyList * * @param string $componentName Name of the component * @param string $filter A filter to be inserted into the WHERE clause @@ -1318,36 +1315,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned. * @param string|array $limit A limit expression to be inserted into the LIMIT clause * - * @return ComponentSet The components of the one-to-many relationship. + * @return HasManyList The components of the one-to-many relationship. */ public function getComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") { $result = null; - $sum = md5("{$filter}_{$sort}_{$join}_{$limit}"); - if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) { - return $this->componentCache[$componentName . '_' . $sum]; - } - if(!$componentClass = $this->has_many($componentName)) { user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR); } $joinField = $this->getRemoteJoinField($componentName, 'has_many'); + + $result = new HasManyList($componentClass, $joinField); + if($this->ID) $result->setForeignID($this->ID); - if($this->isInDB()) { //Check to see whether we should query the db - $query = $this->getComponentsQuery($componentName, $filter, $sort, $join, $limit); - $result = $this->buildDataObjectSet($query->execute(), 'ComponentSet', $query, $componentClass); - if($result) $result->parseQueryLimit($query); - } - - if(!$result) { - // If this record isn't in the database, then we want to hold onto this specific ComponentSet, - // because it's the only copy of the data that we have. - $result = new ComponentSet(); - $this->setComponent($componentName . '_' . $sum, $result); - } - - $result->setComponentInfo("1-to-many", $this, null, null, $componentClass, $joinField); + $result = $result->filter($filter)->limit($limit)->sort($sort)->join($join); return $result; } @@ -1355,11 +1337,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Get the query object for a $has_many Component. * - * Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the - * resultset you're building with this query. - * Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery} - * object, and pass "ComponentSet" as a $containerClass. - * * @param string $componentName * @param string $filter * @param string|array $sort @@ -1419,149 +1396,30 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * and {@link getManyManyComponents()}. * * @param string $componentName Name of the component - * @param DataObject|ComponentSet $componentValue Value of the component + * @param DataObject|HasManyList|ManyManyList $componentValue Value of the component */ public function setComponent($componentName, $componentValue) { $this->componentCache[$componentName] = $componentValue; } /** - * Returns a many-to-many component, as a ComponentSet. - * The return value will be cached on this object instance, - * but only when no related objects are found (to avoid unnecessary empty checks in the database). - * If related objects exist, no caching is applied. - * + * Returns a many-to-many component, as a ManyManyList. * @param string $componentName Name of the many-many component - * @return ComponentSet The set of components + * @return ManyManyList The set of components * * @todo Implement query-params */ public function getManyManyComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") { - $sum = md5("{$filter}_{$sort}_{$join}_{$limit}"); - if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) { - return $this->componentCache[$componentName . '_' . $sum]; - } - list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); - - // Join expression is done on SiteTree.ID even if we link to Page; it helps work around - // database inconsistencies - $componentBaseClass = ClassInfo::baseDataClass($componentClass); - - if($this->ID && is_numeric($this->ID)) { - - if($componentClass) { - $query = $this->getManyManyComponentsQuery($componentName, $filter, $sort, $join, $limit); - $records = $query->execute(); - $result = $this->buildDataObjectSet($records, "ComponentSet", $query, $componentBaseClass); - if($result) $result->parseQueryLimit($query); // for pagination support - if(!$result) { - $result = new ComponentSet(); - } - } - } else { - $result = new ComponentSet(); - } - $result->setComponentInfo("many-to-many", $this, $parentClass, $table, $componentClass); - - // If this record isn't in the database, then we want to hold onto this specific ComponentSet, - // because it's the only copy of the data that we have. - if(!$this->isInDB()) { - $this->setComponent($componentName . '_' . $sum, $result); - } - - return $result; - } - - /** - * Get the query object for a $many_many Component. - * Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the - * resultset you're building with this query. - * Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery} - * object, and pass "ComponentSet" as a $containerClass. - * - * @param string $componentName - * @param string $filter - * @param string|array $sort - * @param string $join - * @param string|array $limit - * @return SQLQuery - */ - public function getManyManyComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") { - list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); - - $componentObj = singleton($componentClass); - - // Join expression is done on SiteTree.ID even if we link to Page; it helps work around - // database inconsistencies - $componentBaseClass = ClassInfo::baseDataClass($componentClass); - - - $query = $componentObj->extendedSQL( - "\"$table\".\"$parentField\" = $this->ID", // filter - $sort, - $limit, - "INNER JOIN \"$table\" ON \"$table\".\"$componentField\" = \"$componentBaseClass\".\"ID\"" // join - ); - foreach((array)$this->many_many_extraFields($componentName) as $extraField => $extraFieldType) { - $query->select[] = "\"$table\".\"$extraField\""; - $query->groupby[] = "\"$table\".\"$extraField\""; - } + $result = new ManyManyList($componentClass, $table, $componentField, $parentField, + $this->many_many_extraFields($componentName)); - if($filter) $query->where[] = $filter; - if($join) $query->from[] = $join; - - return $query; - } - - /** - * Pull out a join clause for a many-many relationship. - * - * @param string $componentName The many_many or belongs_many_many relation to join to. - * @param string $baseTable The classtable that will already be included in the SQL query to which this join will be added. - * @return string SQL join clause - */ - function getManyManyJoin($componentName, $baseTable) { - if(!$componentClass = $this->many_many($componentName)) { - user_error("DataObject::getComponents(): Unknown many-to-many component '$componentName' on class '$this->class'", E_USER_ERROR); - } - $classes = array_reverse(ClassInfo::ancestry($this->class)); - - list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); - - $baseComponentClass = ClassInfo::baseDataClass($componentClass); - if($baseTable == $parentClass) { - return "LEFT JOIN \"$table\" ON (\"$table\".\"$parentField\" = \"$parentClass\".\"ID\" AND \"$table\".\"$componentField\" = '{$this->ID}')"; - } else { - return "LEFT JOIN \"$table\" ON (\"$table\".\"$componentField\" = \"$baseComponentClass\".\"ID\" AND \"$table\".\"$parentField\" = '{$this->ID}')"; - } - } - - function getManyManyFilter($componentName, $baseTable) { - list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); - - return "\"$table\".\"$parentField\" = '{$this->ID}'"; - } - - /** - * Return an aggregate object. An aggregate object returns the result of running some SQL aggregate function on a field of - * this dataobject type. - * - * It can be called with no arguments, in which case it returns an object that calculates aggregates on this object's type, - * or with an argument (possibly statically), in which case it returns an object for that type - */ - function Aggregate($type = null, $filter = '') { - return new Aggregate($type ? $type : $this->class, $filter); - } - - /** - * Return an relationship aggregate object. A relationship aggregate does the same thing as an aggregate object, but operates - * on a has_many rather than directly on the type specified - */ - function RelationshipAggregate($object = null, $relationship = '', $filter = '') { - if (is_string($object)) { $filter = $relationship; $relationship = $object; $object = $this; } - return new Aggregate_Relationship($object ? $object : $this->owner, $relationship, $filter); + // If this is called on a singleton, then we return an 'orphaned relation' that can have the + // foreignID set elsewhere. + if($this->ID) $result->setForeignID($this->ID); + + return $result->filter($filter)->sort($sort)->limit($limit); } /** @@ -2560,7 +2418,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $object = $component->dbObject($fieldName); - if (!($object instanceof DBField) && !($object instanceof ComponentSet)) { + if (!($object instanceof DBField) && !($object instanceof DataList)) { // Todo: come up with a broader range of exception objects to describe differnet kinds of errors programatically throw new Exception("Unable to traverse to related object field [$fieldPath] on [$this->class]"); }