How to Inherit Specific Node Fields in Drupal 7

Content (nodes) in Drupal 7 is generally atomic, in the sense that it’s not by nature related to other content. There are various mechanisms to assign relationships, and one of the more easily understood is the menu hierarchy — e.g. this page is the parent of that page. However, when accessing nodes they don’t tell you about this relationship out-of-the-box, and so how do you set up a section of your site where specific fields are shared or inherited within that section?

An example of this would be that you have multiple departments, and each department’s logo should be displayed across all of its pages (“About”, “Current Events”, etc). There are, of course, many ways to accomplish this — You could set up multiple blocks, one for each logo, or create a special “logo” content type and use taxonomies + views to pull the appropriate logo for related content. For me (and the purposes of this example) it makes the most sense to create a “logo” field for the “department” content type, and if a department logo isn’t specifically set it should fallback to using its parent’s field.

To do this, we need several things (and please correct me if I missed something, because it was wacky):

  1. get the current node
  2. check for a specific field on a node
  3. get a node’s parent, according to the menu

Combining these functions will let us recursively march up the menu tree until we have a value for the specific field. So let’s start with getting the current node:


$node =& menu_get_object();

[source]

Then we need to get a specific field from a given node. Rather than looping through all the properties of the node and their languages, there are two functions I’ve found, each returning slightly different information:

field_get_items('node', $entity, $field_key);

which will return the direct field values from the node, or:

field_view_field('node', $entity, $field_key);

which will return the renderable array of the field instead.

[source 1] [source 2] [source 3]

We can package this into a flexible function (note the examples as given are actually part of a “HELPER” class, so don’t copy/paste them directly):

/**
 * Get a field property from an entity (usually node)
 * 
 * Wrapper for a couple drupal functions to get a language-independent value from a node
 * See references:
 * - http://dominiquedecooman.com/blog/drupal-7-tip-get-field-values-entity-objects-nodes-users-taxonomy
 * - http://drupal7ish.blogspot.com/2011/03/getting-field-data-out-of-entities.html (or use field_extract module)
 * - http://drupal.org/node/250700#comment-4557864
 * 
 * @requires D7
 * @param string $field_key the field identifier (machine name)
 * @param object $entity {default FALSE} if provided, use the given entity (node); if not given will try to get the current node from menu_get_object
 * @param array $options {default: entity_type = 'node' (what is $entity), fetch = 0 (the nth item), property = 'safe_value' (what value prop), `return_type` = field|renderable } extra options
 * 
 * @return the field value or field values, depending on the options (default is single value)
 */
static function get_node_field($field_key, $entity = false, array $options = array() ) {
	$options = array_merge( array(
		'fetch' => 0	// what to fetch; syn: 'single', 'first'
		, 'entity_type' => 'node'
		, 'property' => 'safe_value'
		, 'return_type' => 'field'	// 'renderable'
	), $options);
	
	// default to "current object" - node
	if( ! $entity ) $entity = & menu_get_object();
	
	// if still nothing, skip
	if( ! $entity ) return NULL;
	
	if( 'field' == $options['return_type'] )
		$field = field_get_items($options['entity_type'], $entity, $field_key);
	elseif( 'renderable' == $options['return_type'] )
		$field = field_view_field($options['entity_type'], $entity, $field_key);
	
	if( !isset( $field )) return NULL;	//no value
	
	// are we getting a specific index or all
	if( !isset($options['fetch']) || 'all' === $options['fetch'] ) {
		return $field;
	}
	
	// if we want all the properties...
	if( !isset($options['property']) || 'all' == $options['property'] ) {
		return $field[  $options['fetch']  ];
	}
	// get the requested property for the index
	return $field[  $options['fetch']  ][  $options['property']  ];
}//--	fn	get_node_field

Now comes the dumb part – in order to find the hierarchical relationship, the only reliable way I’ve found is to get the entire menu structure and recursively search it for a specific node, passing the parent back up by reference.

/**
 * Look for the menu item corresponding to the node identifier in the given menu structure
 * 
 * @param int|string $identifier either the node id, or the node title
 * @param array $menu the menu structure, usually through either menu_get_active_trail() or menu_tree_all_data()
 * @param array $attr special attributes/settings
 * 
 * @return the menu index/item as an array(index, item)
 */
static function menu_find_node($identifier, &$menu, $attr = array() ){
	if( empty($menu) ) return false;
	
	// look for either node id or link title
	// if we've found it in this menu structure, then it means our parent item is one level up
	foreach( $menu as $menu_key => $menu_item ) {
		if( isset($attr['menu_source']) && 'menu_trail' == $attr['menu_source'] ) {
			$menu_link = &$menu_item;
		}
		else {
			$menu_link = &$menu_item['link'];
		}
		
		if( isset( $menu_link['link_path'] )
			&&
			(
				( is_numeric($identifier) && 'node/'.$identifier == $menu_link['link_path'] )
				||
				( isset( $menu_link['link_title'] ) && $identifier == $menu_link['link_title'] )
			)
		) {
			return array($menu_key, $menu_item);
		}
	}
	return false;
}//--	fn	menu_find_node
/**
 * Scan a menu object for the given child - if present, return true
 * 
 * Iterates down through a menu object (as returned from menu_tree_all_data), looking for a given menu
 *
 * @param object $menu the menu as returned from menu_tree_all_data
 * @param string|int $child_identifier either the node id or the node title to search for
 * @param array $result the menu parent item, to be passed up by reference through the recursion
 * @param bool $as_node TRUE to pass the result as a node, FALSE to pass the menu item
 *
 * @return TRUE if current menu structure contains the identifier; used by recursion for success state
 */
static function menu_get_parent(&$menu, $child_identifier, &$result, $as_node = true) {
	// look for either node id or link title
	// if we've found it in this menu structure, then it means our parent item is one level up
	if( false !== self::menu_find_node($child_identifier, $menu) ) return true;
	
	// otherwise, check each 'below' recursively for the identifier
	foreach( $menu as $menu_key => $menu_item ) {
		if( isset($menu_item['below']) && true === self::menu_get_parent($menu_item['below'], $child_identifier, $result, $as_node) ) {
			// if we want the menu item, return it
			if( ! $as_node ) {
				if(!isset($result)) $result = $menu_item;	//pass it up the chain, only if it hasn't already been found
				return true;
			}
			// otherwise, turn it into a node
			if(!isset($result)) {
				$nid = str_replace('node/', '', $menu_item['link']['link_path'] );
				$result = node_load($nid);	//pass it up the chain, only if it hasn't already been found
			}
			return true;
		}
	}
	
	return null;	//failure
}//--	fn	menu_get_parent

We combine the previous functions into a wrapper call:

/**
 * Get a field property from a node, marching up/down the menu hierarchy until a field is provided
 * 
 * Wrapper for a couple drupal functions to get a language-independent value from a node;
 * checking up/down the menu hierarchy until a relative has this field
 * See references:
 * - http://dominiquedecooman.com/blog/drupal-7-tip-get-field-values-entity-objects-nodes-users-taxonomy
 * - http://drupal7ish.blogspot.com/2011/03/getting-field-data-out-of-entities.html (or use field_extract module)
 * - http://drupal.org/node/250700#comment-4557864
 * 
 * @requires D7
 * @param string $field_key the field identifier (machine name)
 * @param object $relative {default FALSE} if provided, use the given entity (node); if not given will try to get the current node from menu_get_object; in recursive calls, used for marching up/down the menu tree
 * @param array $options {default: entity_type = 'node' (what is $entity), fetch = 0 (the nth item), property = 'safe_value' (what value prop) } extra options
 * 
 * @return the field value or field values, depending on the options (default is single value)
 */
static function get_node_field__recursive($field_key, $relative = false, $options = array() ){
	// loop starts with current node (via get_node_field where $relative = false),
	//	continues as long as we keep finding parents/children and the $feed_key isn't found
	while( ! isset( $field_value ) || NULL !== $field_value || ( isset($options['ignore']) && in_array($field_value, $options['ignore']) ) ) :
		$field_value = self::get_node_field($field_key, $relative, $options);

		if( isset($options['relation']) && 'child' == $options['relation'] ) {
			throw new EntityFieldQueryException('Method not implemented - child relation');
		}
		else /* if ('parent' == $options['relation']) */ {
			$relative = self::get_node_parent($relative);
		}
		
		// stop if we've run out of menu items
		if( ! $relative ) break;
		
	endwhile;	// no value given, or default (which we don't want)
	
	return $field_value;
}//--	fn	get_node_field__recursive

Then you can fetch the potentially inherited value using the following:

$whatever = HELPER::get_node_field__recursive('field_WHATEVER', false
	, array(
		'fetch' => 'all' // 'all' | 0 | a "#"
		// , 'property' => 'safe_value'
		, 'relation'=>'parent'
		, 'ignore'=>array('SOME DEFAULT VALUE')
		)
	);

Please note that there are probably modules that do this already, like Node Hierarchy, but I wanted to try this from scratch instead.

2 thoughts on “How to Inherit Specific Node Fields in Drupal 7

  1. Great writeup. It all makes sense, but I’m not able to get this working correctly. Where do you have this function? I tried template.php but it blew up my site.

    • 2x sorry (problems+lateness); I’m guessing the “static function” part and a couple namespace bugs are what killed your attempt. I probably should have mentioned earlier that I have these functions as part of a helper class (see last snippet, line 1), so you can’t just copy-paste this directly into your template.php file, you’d have to put these functions inside a class (ideally in a separate file you include in your template.php). I think I fixed the errors in the example as well.

Leave a Reply

Your email address will not be published. Required fields are marked *