Counting Menu Items & Determining If First/Last

I’m hoping the SitePoint community can help where other forums have been unable to!

I am attempting to have my code count how many top level menu items there are and then determine what position each menu item is in. So, if it is the first, it will append the class of that item to “first” and if it is the last, it will append the class to “last.”

I did the same thing with other variables and it worked. For example, I have the code set to append the class to “parent” if there are subitems and also “active” if it is the current link. However, I am running into difficulty with first/last. I’ve been through several attempts. My first incarnation was:

<?php
/*
    Class: MenuDefault
        Menu base class
*/
class MenuDefault extends Menu {
    /*
        Function: process
        Returns:
            Object
    */
    public function process($module, $element) {
        self::_process($module, $element->first('ul:first'));
        return $element;
    }
    /*
        Function: _process
        Returns:
            Void
    */
    protected static function _process($module, $element, $level = 0) {
        if ($level == 0) {
            $element->attr('class', 'menu '.$module->menu_style);
        } else {
            $element->addClass('level'.($level + 1));
        }
        foreach ($element->children('li') as $li) {
             // is active ?
            if ($active = $li->attr('data-menu-active')) {
                $active = $active == 2 ? ' active current' : ' active';
            }
             // is parent ?
            $ul = $li->children('ul');
            $parent = $ul->length ? ' parent' : null;
            // is first or last ? -- PROBLEM AREA
            $lis = $element->children("li");
             for($forl=0,$imax=$lis;
             $forl<$imax;$forl++){
             if ($forl==0) $position_n = 'first';
             elseif ($forl==$imax-1) $position_n = 'last';
             else $position_n = null;}
              // set class in li
            $li->attr('class', sprintf('level%d item%s '. $position_n .$parent.$active, $level + 1, $li->attr('data-id')));
            // set class in a/span
            foreach ($li->children('a,span') as $child) {
                // get title
                $title = $child->first('span:first');
                // set subtile
                $subtitle = $title ? explode('||', $title->text()) : array();
                if (count($subtitle) == 2) {
                    $li->addClass('hassubtitle');
                    $title->html(sprintf('<span class="title">%s</span><span class="subtitle">%s</span>', trim($subtitle[0]), trim($subtitle[1])));
                }
                // set image
                if ($image = $li->attr('data-menu-image')) {
                    $title->prepend(sprintf('<span class="icon" style="background-image: url(\\'%s\\');"> </span>', $image));
                }
                $child->addClass(sprintf('level%d'.$parent.$active, $level + 1));
            }
            // process submenu
            if ($ul->length) {
                self::_process($module, $ul->item(0), $level + 1);
            }
        }
   }
}

However, that generated “first” for every menu item. See below for example:

<ul class="menu menu-dropdown">
  <li class="level1 item24 first">...Subcode...</li>
  <li class="level1 item22 first parent">...Subcode...</li>
  <li class="level1 item23 first active current">...Subcode...</li>
  <li class="level1 item20 first">...Subcode...</li>
  <li class="level1 item19 first">...Subcode...</li>
</ul>

What I wanted was:

<ul class="menu menu-dropdown">
  <li class="level1 item24 first">...Subcode...</li>
  <li class="level1 item22 parent">...Subcode...</li>
  <li class="level1 item23 active current">...Subcode...</li>
  <li class="level1 item20">...Subcode...</li>
  <li class="level1 item19 last">...Subcode...</li>
</ul>

Then in another forum, it was suggested to me that " Your basic logic for determining first and last is correct, but you should not add a new loop to do it; instead you need to make it part of the loop that is already looping over the <li>'s (ie: the foreach loop at the top of the snippet I posted). Because of the use of the foreach statement, you need to do a little extra work in order to obtain numerically useful index values.

// is first or last ? -- PROBLEM AREA
$children_elements = array_values($element->children('li')); [/U]
$last_child_index = count($children_elements)-1;
foreach($children_elements as $child_index => $li) {
    if ($forl==0) $position_n = 'first';
    elseif ($forl==$imax-1) $position_n = 'last';
    else $position_n = null;}  

That brought a bunch of errors

Warning: array_values() [
function.array-values
]: The argument should be an array in … on line 59

Warning: Invalid argument supplied for foreach() in … on line 61

Warning: array_values() [
function.array-values
]: The argument should be an array in … on line 59

Warning: Invalid argument supplied for foreach() in … on line 61

Warning: array_values() [
function.array-values
]: The argument should be an array in … on line 59

Warning: Invalid argument supplied for foreach() in … on line 61

Warning: array_values() [
function.array-values
]: The argument should be an array in … on line 59

Warning: Invalid argument supplied for foreach() in … on line 61

Warning: array_values() [
function.array-values
]: The argument should be an array in … on line 59

Warning: Invalid argument supplied for foreach() in … on line 61

Warning: array_values() [
function.array-values
]: The argument should be an array in … on line 59

Warning: Invalid argument supplied for foreach() in … on line 61

Warning: array_values() [
function.array-values
]: The argument should be an array in … on line 59

Line 59 being $children_elements = array_value($element->children(‘li’));

So it seemed like $children_elements was either empty or not an array so I tested with this:

if(empty($children_elements)) {
    echo hi;
}  

Hi was generated. So it seems empty.

Sorry if this was long but I had no idea how to explain quicker. Please help. I’m stumped and if I can’t get this working, a large portion of my time will go to waste as I’ll have to scrap the theme design completely.

Thanks in advance.

I couldn’t figure it out the way I originally wanted to do it. But I knew that there must be some native wordpress functionality I could use with wp_nav_menu so I created this code and it works well to have wordpress count and mark first/last (excluding child items for last) with an added function and filter.

Seems to work well. Here it is for anyone with a similar issue.

// set first/last class
function nb_first_last_menu_class( $objects, $args ) {
 	// set first/last class for submenu
    $ids        = array();
    $parent_ids = array();
    $top_ids    = array();
    foreach ( $objects as $i => $object ) {
        // If no parent, store the ID, skip object
        if ( 0 == $object->menu_item_parent ) {
            $top_ids[$i] = $object;
            continue;
        }
        // don't set first class for submenu
        if ( ! in_array( $object->menu_item_parent, $ids ) ) {
            $objects[$i]->classes[] = '';
            $ids[]          = $object->menu_item_parent;
        }
 		// If we didn't set first class for submenu, skip adding the ID
        if ( in_array( '', $object->classes ) )
            continue;
         // Store the menu parent IDs in an array
        $parent_ids[$i] = $object->menu_item_parent;
    }
    // Remove dup values and pull out last menu item
    $sanitized_parent_ids = array_unique( array_reverse( $parent_ids, true ) );
    // Loop IDs, set last class to the appropriate objects
    foreach ( $sanitized_parent_ids as $i => $id )
        $objects[$i]->classes[] = 'last';
    // set classes for top level menu items
    $objects[1]->classes[] = 'first';
    $objects[end( array_keys( $top_ids ) )]->classes[] = 'last';
    return $objects;
}
add_filter( 'wp_nav_menu_objects', 'nb_first_last_menu_class', 10, 2 );