Perforce Chronicle 2012.2/486814
API Documentation

P4Cms_Menu Class Reference

Menus provide persistent storage for Navigation Containers. More...

Inheritance diagram for P4Cms_Menu:
P4Cms_Record_Config P4Cms_Record P4Cms_Record_Connected P4Cms_Model P4Cms_ModelInterface

List of all members.

Public Member Functions

 addDefaultEntry (array $entry, $entryId, Zend_Navigation_Container $container=null)
 Add a menu entry from a package to the set of existing menu items.
 addPage ($page)
 Add a page to the raw navigation container in this menu.
 diff (P4Cms_Menu $menu)
 Diff our menu instance against the given menu.
 getContainer ()
 Get this menu's raw navigation container (dynamic items will be left unexpanded).
 getExpandedContainer ($options=array())
 Based on the current config, returns the full Navigation Container; Dynamic items will be replaced with their expanded value(s).
 getLabel ()
 Get the human friendly menu name.
 merge (P4Cms_Menu $theirs, P4Cms_Menu $base)
 Merge the given menu into this menu.
 recursiveCount (Zend_Navigation_Container $container)
 Helper to count all of the items in a navigation container recursively.
 removeDefaultEntry (array $entry, $entryId, Zend_Navigation_Container $container=null)
 Remove items introduced via addDefaultEntry().
 save ($description=null)
 Save this menu.
 setContainer ($container)
 Sets the raw Navigation Container.
 setLabel ($label)
 Set a human friendly menu name.
 trimContainer ($container, $maxDepth, $maxItems)
 Trim the given navigation container according to the passed maximum depth and maximum items limits.

Static Public Member Functions

static fetchAll ($query=null, P4Cms_Record_Adapter $adapter=null)
 Get all menus.
static fetchDefault (P4Cms_Record_Adapter $adapter=null)
 Fetch the default menu.
static fetchMenuOrHandlerAsMenu ($id)
 Fetches a menu instance even if given a dynamic handler id.
static fetchMixed ($query=null, P4Cms_Record_Adapter $adapter=null)
 Retrieve all menus and all menu items in a single flat list.
static getDefaultMenuIds ()
 Get the ids of all menus contributed by active packages.
static getItemId ($item)
 Determine the unique identifier of the given menu item.
static installDefaultMenus ($limit=null, P4Cms_Record_Adapter $adapter=null)
 Collect all of the default menus/items and install any that are missing.
static installPackageDefaults (P4Cms_PackageAbstract $package, $limit=null, P4Cms_Record_Adapter $adapter=null)
 Install the default menus contributed by a package.
static isDynamicHandlerId ($id)
 Determine if the given id represents a dynamic handler id.
static removePackageDefaults (P4Cms_PackageAbstract $package, P4Cms_Record_Adapter $adapter=null)
 Remove the default menus contributed by a package.

Public Attributes

const DEFAULT_MENU = 'primary'
const ITEM_ORDER_PADDING = 10
const MENU_KEEP_ROOT = 'keepRoot'
const MENU_MAX_DEPTH = 'maxDepth'
const MENU_MAX_ITEMS = 'maxItems'
const MENU_ROOT = 'root'

Protected Member Functions

 _expandContainer ($original, $expanded, &$options)
 Copy items from the given 'original' container to the given 'expanded' container, expanding dynamic menu items as we go.
 _expandDynamic ($dynamic, &$options)
 Expand the given dynamic item via the expansion callback and return the replacement items.
 _getHandler ($handler)
 Get the named dynamic handler from our local cache.
 _getHandlers ()
 Get all of the dynamic handlers.
 _getPageProperties ($page, array $exclude=null)
 Get page properties suitable for merging with another page.
 _normalizeOptions ($options)
 Process expansion options to ensure consistent entries and values.
 _passesAcl ($item)
 Check if the current user is allowed to access the given menu item.

Protected Attributes

 $_container = null

Static Protected Attributes

static $_fields
 Specifies the array of fields that the current Record class wishes to use.
static $_handlers = null
static $_storageSubPath = 'menus'
 Specifies the sub-path to use for storage of records.

Detailed Description

Menus provide persistent storage for Navigation Containers.

Additionally, they handle the expansion of 'dynamic' items and assist in installing default menus.

Dynamic menu items provide a means injecting variable items. At display time they can be replaced with zero or more actual navigation pages or containers.

Copyright:
2011-2012 Perforce Software. All rights reserved
License:
Please see LICENSE.txt in top-level folder of this distribution.
Version:
2012.2/486814

Member Function Documentation

P4Cms_Menu::_expandContainer ( original,
expanded,
&$  options 
) [protected]

Copy items from the given 'original' container to the given 'expanded' container, expanding dynamic menu items as we go.

Calls itself to expand dynamic menu items at any depth.

Options are passed by reference so this function can recursively decrement max-items.

Parameters:
P4Cms_Navigation$originalthe original (unexpanded) navigation container.
P4Cms_Navigation$expandedthe new (expanded) navigation container.
array&$optionsnormalized options (
See also:
_normalizeOptions).
Returns:
void
    {
        $maxDepth =& $options[self::MENU_MAX_DEPTH];
        $maxItems =& $options[self::MENU_MAX_ITEMS];
        $root     =  $options[self::MENU_ROOT];
        $rooted   =  false;

        // if a root has been specified, find root by matching against UUID and
        // update 'original' to point to found item - return if root can't be found.
        $uuid = reset(explode('/', $root, 2));
        if (!empty($uuid)) {
            $recursive = new RecursiveIteratorIterator(
                $original,
                RecursiveIteratorIterator::SELF_FIRST
            );
            foreach ($recursive as $item) {
                if (static::getItemId($item) === $uuid) {
                    $original = $item;
                    $rooted   = true;

                    // strip UUID from root so we don't re-parse it when recursing.
                    $options[self::MENU_ROOT] = substr($root, strlen($uuid));
                    break;
                }
            }

            // if we didn't find the root, return early.
            if ($original !== $item) {
                return;
            }
        }

        // if we are rooted and options stipulate we keep the root, push it down
        // we don't push down dynamic items because that is handled elsewhere
        if ($rooted
            && $options[self::MENU_KEEP_ROOT]
            && !$original instanceof P4Cms_Navigation_Page_Dynamic
        ) {
            $original = new P4Cms_Navigation(array($original));
        }

        // if the original container is a empty dynamic item, push it down a level.
        // (no relation to keep root above - this is done because dynamic items are
        // expanded in place of the dynamic item, not as children)
        if ($original instanceof P4Cms_Navigation_Page_Dynamic
            && !$original->hasPages()
        ) {
            $original = new P4Cms_Navigation(array($original));
        }

        // loop over original menu items and copy them to the
        // expanded container, expanding dynamic items as we go.
        // note: we explicitly set item order to preserve the original
        // order and prevent jostling of items with equal weight.
        $order = 0;
        foreach ($original as $item) {

            // skip items the user should not see.
            if (!$this->_passesAcl($item)) {
                continue;
            }

            // if we already hit max items, stop processing items.
            if ($maxItems !== null && $maxItems < 1) {
                break;
            }

            // if item is dynamic, expand it.
            if ($item instanceof P4Cms_Navigation_Page_Dynamic) {
                try {

                    // forcibly copy options before we pass them to the callback
                    // we do this because we use references above and any option
                    // changes in the callback would affect us here (due to the
                    // peculiar behavior of references in PHP).
                    $itemOptions = unserialize(serialize($options));

                    // dynamic items can be configured with max-depth/items
                    // options - we want to take the more restrictive of the
                    // item options vs. the options provided by the caller.
                    // (take the lowest integer value for max-items/depth)
                    $itemLimits  = array_filter(array($item->get(self::MENU_MAX_ITEMS), $maxItems), 'is_int');
                    $depthLimits = array_filter(array($item->get(self::MENU_MAX_DEPTH), $maxDepth), 'is_int');
                    $itemOptions[self::MENU_MAX_ITEMS] = $itemLimits  ? min($itemLimits)  : null;
                    $itemOptions[self::MENU_MAX_DEPTH] = $depthLimits ? min($depthLimits) : null;

                    // we want to copy certain dynamic item properties to
                    // the expanded replacement items.
                    $properties = $this->_getPageProperties($item);

                    // move replacements to expanded container.
                    // use iterator_to_array because moving pages upsets the loop.
                    $replacements = $this->_expandDynamic($item, $itemOptions);
                    foreach (iterator_to_array($replacements) as $replacement) {

                        // skip replacement items the user should not see.
                        if (!$this->_passesAcl($replacement)) {
                            continue;
                        }

                        // merge original dynamic properties with replacement
                        $replacement->setOptions(
                            array_merge(
                                $properties,
                                $this->_getPageProperties($replacement)
                            )
                        );

                        $replacement->setOrder($order++);
                        $expanded->addPage($replacement);
                    }

                } catch (Exception $e) {
                    P4Cms_Log::logException("Failed to expand dynamic menu item.", $e);
                }

                // next item - dynamic items don't get added
                continue;
            }

            // standard item, copy and add to expanded container (w. out children).
            $itemCopy = clone $item;
            $expanded->addPage($itemCopy);
            $itemCopy->setOrder($order++)
                     ->removePages();
            $maxItems--;

            // if the item has sub-pages, expand them as well
            // decrement maxdepth as we go deeper.
            if ($item->hasPages() && ($maxDepth === null || $maxDepth > 0)) {
                $maxDepth = $maxDepth === null ? null : $maxDepth - 1;
                $this->_expandContainer($item, $itemCopy, $options);
                $maxDepth = $maxDepth === null ? null : $maxDepth + 1;
            }
        }
    }
P4Cms_Menu::_expandDynamic ( dynamic,
&$  options 
) [protected]

Expand the given dynamic item via the expansion callback and return the replacement items.

Options are passed by reference. The max-items option will be decremented by the total number of replacement items.

Parameters:
P4Cms_Navigation_Page_Dynamic$dynamicthe dynamic item to expand.
array&$optionsnormalized options (
See also:
_normalizeOptions).
Returns:
Zend_Navigation_Container the expanded items honoring expansion options.
    {
        // if the dynamic item does not specify a valid handler, nothing to do.
        $handler = $this->_getHandler($dynamic->getHandler());
        if (!$handler) {
            return new P4Cms_Navigation;
        }

        // get replacement items via handler callback.
        $root = $options[self::MENU_ROOT];
        $options[self::MENU_ROOT] = pack("H*", substr($root, 1));
        $replacements = $handler->callExpansionCallback($dynamic, $options);
        $options[self::MENU_ROOT] = $root;

        // normalize to a navigation container.
        if (!$replacements instanceof Zend_Navigation_Container) {
            $replacements = new P4Cms_Navigation($replacements);
        }

        // associate dynamic item with each expanded item and search
        // for root if one has been specified.
        $recursive = new RecursiveIteratorIterator(
            $replacements,
            RecursiveIteratorIterator::SELF_FIRST
        );
        unset($root);
        foreach ($recursive as $item) {
            $item->dynamic = $dynamic;

            if (!empty($options[self::MENU_ROOT])
                && !isset($root)
                && static::getItemId($item) === $dynamic->uuid . $options[self::MENU_ROOT]
            ) {
                $root = $item;
            }
        }

        // if a root has been found, update 'replacements' to point to found item
        // if a root was specified, but not found, return empty container.
        if (isset($root) && $root instanceof Zend_Navigation_Container) {
            $replacements = $root;
        } else if (!empty($options[self::MENU_ROOT])) {
            return new P4Cms_Navigation;
        }

        // if the options stipulate we should keep the root, push it down.
        if (isset($root) && $options[self::MENU_KEEP_ROOT]) {
            $replacements = new P4Cms_Navigation(array($replacements));
        }

        // trim replacement items according to max-depth and max-items.
        $itemCount = $this->trimContainer(
            $replacements,
            $options[self::MENU_MAX_DEPTH],
            $options[self::MENU_MAX_ITEMS]
        );

        // options are passed by reference, decrement max-items
        if ($options[self::MENU_MAX_ITEMS] !== null) {
            $options[self::MENU_MAX_ITEMS] -= $itemCount;
        }

        return $replacements;
    }
P4Cms_Menu::_getHandler ( handler) [protected]

Get the named dynamic handler from our local cache.

Parameters:
string$handlerthe name of the handler to get.
Returns:
P4Cms_Navigation_DynamicHandler|null the requested handler or null.
    {
        $handlers = $this->_getHandlers();
        return isset($handlers[$handler]) ? $handlers[$handler] : null;
    }
P4Cms_Menu::_getHandlers ( ) [protected]

Get all of the dynamic handlers.

Only fetches handlers the first time and caches them for subsequent calls.

Returns:
P4Cms_Model_Iterator the dynamic handlers in the system.
    {
        if (!static::$_handlers) {
            static::$_handlers = P4Cms_Navigation_DynamicHandler::fetchAll();
        }

        return static::$_handlers;
    }
P4Cms_Menu::_getPageProperties ( page,
array $  exclude = null 
) [protected]

Get page properties suitable for merging with another page.

Excludes type info, sub-pages and any empty properties by default - pass an array of properties to override.

This method is needed because the Zend_Navigation_Page toArray() method calls toArray() on all sub-pages recursively.

Parameters:
array | Zend_Navigation_Page$pagethe page to get mergeable properties from.
array$excludeoptional - pass to over-ride default excluded
Returns:
array the mergeable properties of the page.
    {
        // convert page objects to array form.
        // strip pages first to avoid extra work.
        if ($page instanceof Zend_Navigation_Page) {
            $page = clone $page;
            $page->removePages();
            $page = $page->toArray();
        }

        // must have an array at this point.
        if (!is_array($page)) {
            throw new InvalidArgumentException(
                "Cannot get properties. Page must be an array or page instance."
            );
        }

        // strip out the excluded or empty properties
        $exclude = is_null($exclude) ? array('pages', 'type', 'typeInferred') : $exclude;
        $filter  = array_fill_keys($exclude, null);
        $values  = array_filter(array_merge($page, $filter));

        return $values;
    }
P4Cms_Menu::_normalizeOptions ( options) [protected]

Process expansion options to ensure consistent entries and values.

Parameters:
array$optionsthe expansion options to normalize.
Returns:
array the normalized expansion options.
    {
        $normalized = array(
            static::MENU_MAX_DEPTH        => null,
            static::MENU_MAX_ITEMS        => null,
            static::MENU_KEEP_ROOT        => false,
            static::MENU_ROOT             => null,
        );

        if (isset($options[static::MENU_MAX_DEPTH])
            && strlen($options[static::MENU_MAX_DEPTH])
            && intval($options[static::MENU_MAX_DEPTH]) >= 0
        ) {
            $normalized[static::MENU_MAX_DEPTH] = intval($options[static::MENU_MAX_DEPTH]);
        }

        if (isset($options[static::MENU_MAX_ITEMS])
            && strlen($options[static::MENU_MAX_ITEMS])
            && intval($options[static::MENU_MAX_ITEMS]) > 0
        ) {
            $normalized[static::MENU_MAX_ITEMS] = intval($options[static::MENU_MAX_ITEMS]);
        }

        if (isset($options[static::MENU_ROOT])
            && !empty($options[static::MENU_ROOT])
        ) {
            $normalized[static::MENU_ROOT] = $options[static::MENU_ROOT];
        }

        if (isset($options[static::MENU_KEEP_ROOT])) {
            $normalized[static::MENU_KEEP_ROOT] = $options[static::MENU_KEEP_ROOT];
        }

        // the options we care about will be normalized,
        // any other options will be merged in as-is.
        return $normalized + (array) $options;
    }
P4Cms_Menu::_passesAcl ( item) [protected]

Check if the current user is allowed to access the given menu item.

Parameters:
Zend_Navigation_Page$itemthe item to check access for.
Returns:
bool true if the active user can access item.
    {
        // if item has no acl resource, nothing to check.
        if (!$item->getResource()) {
            return true;
        }

        // if no active user, can't check acl - assume the worst.
        if (!P4Cms_User::hasActive()) {
            return false;
        }

        return P4Cms_User::fetchActive()->isAllowed(
            $item->getResource(),
            $item->getPrivilege()
        );
    }
P4Cms_Menu::addDefaultEntry ( array $  entry,
entryId,
Zend_Navigation_Container $  container = null 
)

Add a menu entry from a package to the set of existing menu items.

Operates recursively, adding any sub-pages in the same fashion.

If a container is specified, the entry will be added as a sub-page of that container; otherwise, the entry is placed at the top-level of this menu.

A predictable UUID is generated from the given entry id; this allows us to find this particular menu item in the future. This is needed so that we can remove package defaults when a package is disabled. It is also needed during this install process because multiple packages may contribute to the same menu item and we need to be able to locate the item consistently.

For example:

foo/module.ini [menus] header.links.label = Links

bar/module.ini [menus] header.links.pages.home.label = Home header.links.pages.home.uri = /

The entry ids are formed from the declared array keys (e.g. 'links/home' for the home page link). Without the ids, we would have no way of correlating menu structures across packages (normally Zend Navigation entries are completely anonymous).

Parameters:
array$entrythe menu item definition
string$entryIdthe identifier for finding this item
Zend_Navigation_Container$containeroptional - container to insert item into
Returns:
P4Cms_Menu To maintain a fluent interface
    {
        // target container defaults to top-level of menu
        $container = $container ?: $this->getContainer();

        // define the new page item, excluding sub-pages
        // note: we use the entry id to make a predictable
        // uuid so that we can find this page in the future.
        $uuid               = P4Cms_Uuid::fromMd5(md5($entryId))->get();
        $newPage            = $entry;
        $newPage['pages']   = array();
        $newPage['uuid']    = $uuid;

        // if this entry doesn't exist, create it;
        // otherwise, merge with (and re-create) the existing entry.
        if (!$oldPage = $this->getContainer()->findBy('uuid', $uuid)) {
            $newPage = P4Cms_Navigation::inferPageType($newPage);
            $container->addPage($newPage);
        } else {
            // merge old-page with new-page.
            $newPage = array_merge(
                $this->_getPageProperties($oldPage),
                $newPage
            );
            $newPage['pages'] = $oldPage->getPages();

            // re-assess page type (this is a bit tricky)
            // we want explicit types to win, so there are a few cases:
            //  a. new page has explicit type, use it.
            //  b. old page has explicit type, keep it.
            //  c. no explicit type, infer it.
            if (isset($entry['type'])) {
                $newPage['type'] = $entry['type'];
            } else if (!$oldPage->get('typeInferred')) {
                $newPage['type'] = get_class($oldPage);
            } else {
                $newPage = P4Cms_Navigation::inferPageType($newPage);
            }

            // replace old-page with new-page.
            $container->removePage($oldPage);
            $container->addPage($newPage);
        }

        $page = $container->findBy('uuid', $uuid);

        // if the given entry has sub-entries, add them too (recursively)
        if (isset($entry['pages']) && is_array($entry['pages'])) {
            foreach ($entry['pages'] as $subEntryId => $subEntry) {
                $this->addDefaultEntry(
                    $subEntry,
                    $entryId . "/" . $subEntryId,
                    $page
                );
            }
        }

        return $this;
    }
P4Cms_Menu::addPage ( page)

Add a page to the raw navigation container in this menu.

Parameters:
array | Zend_Navigation_Page | Zend_Config$pagea page to add to the menu.
Returns:
P4Cms_Menu provides fluent interface.
    {
        try {
            $this->getContainer()->addPage($page);
        } catch (Exception $e) {
            P4Cms_Log::log('failed to add page:'. print_r($page, true), P4Cms_Log::DEBUG);
            throw $e;
        }

        return $this;
    }
P4Cms_Menu::diff ( P4Cms_Menu menu)

Diff our menu instance against the given menu.

This will produce a flat list of diff details for items in either this menu or the given menu (keyed by UUID).

Each diff detail will have the following elements:

type: same|change|insert|delete (purely positional changes are 'same') isMove: boolean flag indicating that positional properties differ left: item from the given menu (null if 'insert') right: item from our instance menu (null if 'delete')

Parameters:
P4Cms_Menu$menuthe menu to diff against
Returns:
array diff details for items in either menu
    {
        $menu = $menu->getContainer();

        // simple function to flatten container and index
        // result by uuid for quicker lookup
        $flatten = function($container)
        {
            $recursive = new RecursiveIteratorIterator(
                $container,
                RecursiveIteratorIterator::SELF_FIRST
            );

            $items = array();
            foreach ($recursive as $item) {
                if (empty($item->uuid)) {
                    continue;
                }
                $items[$item->uuid] = $item;
            }

            return $items;
        };

        // returns positional properties (order/parent)
        $getPositionValues = function($item)
        {
            $parent = $item->getParent();
            $parent = $parent instanceof Zend_Navigation_Page ? $parent->uuid : null;
            return array('parent' => $parent, 'order' => $item->order);
        };

        $left  = $flatten($menu);
        $right = $flatten($this->getContainer());
        $both  = array_keys($left + $right);
        $diffs = array();
        foreach ($both as $uuid) {
            $isMove    = false;
            $leftItem  = isset($left[$uuid])  ? $left[$uuid]  : null;
            $rightItem = isset($right[$uuid]) ? $right[$uuid] : null;

            // determine the type of difference (if there is one)
            //  - if item not in left, this is an insert.
            //  - if item is not in right, this is a delete.
            //  - if we have left and right
            //  -- if non-positional properties differ, type is 'change'
            //  -- if non-positional properties match, type is 'same'.
            //  -- additionally, if positional values differ, flag as move
            if (!$leftItem) {
                $type = 'insert';
            } else if (!$rightItem) {
                $type = 'delete';
            } else {
                $leftValues  = $this->_getPageProperties($leftItem, array('order'));
                $rightValues = $this->_getPageProperties($rightItem, array('order'));
                $type = $leftValues == $rightValues ? 'same' : 'change';

                // flag item as moved if the position has changed
                $isMove = $getPositionValues($leftItem) != $getPositionValues($rightItem);
            }

            $diffs[$uuid] = array(
                'type'   => $type,
                'isMove' => $isMove,
                'left'   => $leftItem,
                'right'  => $rightItem
            );
        }

        return $diffs;
    }
static P4Cms_Menu::fetchAll ( query = null,
P4Cms_Record_Adapter adapter = null 
) [static]

Get all menus.

Extended to sort by 'order' and 'label' by default.

Parameters:
P4Cms_Record_Query | array | null$queryoptional - query options to augment result.
P4Cms_Record_Adapter$adapteroptional - storage adapter to use.
Returns:
P4Cms_Model_Iterator all records of this type.

Reimplemented from P4Cms_Record.

    {
        $query = static::_normalizeQuery($query);
        $menus = parent::fetchAll($query, $adapter);

        // if no sorting options in the query, sort by order then label.
        if (!$query->getSortBy()) {
            $menus->sortBy(
                array(
                    'order' => array(P4Cms_Model_Iterator::SORT_NUMERIC),
                    'label' => array(P4Cms_Model_Iterator::SORT_NATURAL)
                )
            );
        }

        return $menus;
    }
static P4Cms_Menu::fetchDefault ( P4Cms_Record_Adapter adapter = null) [static]

Fetch the default menu.

If the default menu has been removed, returns a new in-memory menu with the default menu id.

Parameters:
P4Cms_Record_Adapter$adapteroptional - storage adapter to use.
    {
        if (static::exists(static::DEFAULT_MENU, $adapter)) {
            $menu = static::fetch(static::DEFAULT_MENU, null, $adapter);
        } else {
            $menu = new static;
            $menu->setId(static::DEFAULT_MENU);
        }

        return $menu;
    }
static P4Cms_Menu::fetchMenuOrHandlerAsMenu ( id) [static]

Fetches a menu instance even if given a dynamic handler id.

In some higher-level code, occassionally we need to get a menu instance from an identifier that might represent a menu id OR a dynamic handler id (

See also:
isDynamicHandlerId for details).

This method will determine what type of id we are looking at. If it is a plain menu id, it will attempt to fetch the menu. If it is a dynamic handler id, it will attempt to fetch the handler and place it into a new menu instance.

Parameters:
string$idthe menu or dynamic handler id
Returns:
P4Cms_Menu the fetched or created menu instance
Exceptions:
P4Cms_Model_NotFoundExceptionif id is not a valid menu or handler id.
    {
        $handlerId = static::isDynamicHandlerId($id);
        if ($handlerId) {

            // fetch the handler (to ensure it's valid) and put it in a page
            // so that we can stuff it in a menu.
            $handler       = P4Cms_Navigation_DynamicHandler::fetch($handlerId);
            $menu          = new P4Cms_Menu;
            $page          = new P4Cms_Navigation_Page_Dynamic;
            $page->handler = $handler->getId();

            // we need to give the page a contrived uuid so that it can be
            // identified consistently (e.g. for menu root purposes) the
            // handler id seems a good choice and is encoded to ensure it
            // doesn't contain any unexpected characters that would break
            // the code that splits uuid's from their dynamic expansion ids
            // @see getItemId
            $page->uuid = bin2hex($handler->getId());

            $menu->addPage($page);
        } else {
            $menu = static::fetch($id);
        }

        return $menu;
    }
static P4Cms_Menu::fetchMixed ( query = null,
P4Cms_Record_Adapter adapter = null 
) [static]

Retrieve all menus and all menu items in a single flat list.

Both menus and menu items will be wrapped in a P4Cms_Menu_Mixed Model to normalize them.

Parameters:
P4Cms_Record_Query | array | null$queryoptional - query options to augment result.
P4Cms_Record_Adapter$adapteroptional - storage adapter to use.
Returns:
P4Cms_Model_Iterator menus and menu items in a single flat list.
    {
        $items = new P4Cms_Model_Iterator;
        $menus = P4Cms_Menu::fetchAll($query, $adapter);

        foreach ($menus as $menu) {
            $mixed = new P4Cms_Menu_Mixed;
            $mixed->setMenu($menu);
            $items[] = $mixed;

            $pages = new RecursiveIteratorIterator(
                $menu->getContainer(),
                RecursiveIteratorIterator::SELF_FIRST
            );

            foreach ($pages as $page) {
                $mixed = new P4Cms_Menu_Mixed;
                $mixed->setMenu($menu);
                $mixed->setMenuItem($page);
                $mixed->setDepth($pages->getDepth() + 1);

                // if this page doesn't live directly under the menu,
                // assign the parent menu item.
                if ($pages->getDepth()) {
                    $mixed->setParentMenuItem($pages->getSubIterator());
                }

                $items[] = $mixed;
            }
        }

        return $items;
    }
P4Cms_Menu::getContainer ( )

Get this menu's raw navigation container (dynamic items will be left unexpanded).

Returns:
P4Cms_Navigation this menu's (unexpanded) nav container.
    {
        // load container from config (once).
        if (!$this->_container) {
            $this->_container = new P4Cms_Navigation($this->getConfig()->container);
        }

        return $this->_container;
    }
static P4Cms_Menu::getDefaultMenuIds ( ) [static]

Get the ids of all menus contributed by active packages.

Returns:
array a list of ids of default menus.
    {
        // get all enabled modules.
        $packages = P4Cms_Module::fetchAllEnabled();

        // add current theme to packages
        if (P4Cms_Theme::hasActive()) {
            $packages[] = P4Cms_Theme::fetchActive();
        }

        $ids = array();
        foreach ($packages as $package) {
            $ids = array_merge($ids, array_keys($package->getMenus()));
        }

        return array_unique($ids);
    }
P4Cms_Menu::getExpandedContainer ( options = array())

Based on the current config, returns the full Navigation Container; Dynamic items will be replaced with their expanded value(s).

Parameters:
array$optionsoptional - flags to augment the contents of the navigation container - supported options include:

MENU_MAX_DEPTH - limit the depth of the container - a depth of zero will only include top level items.

Returns:
P4Cms_Navigation The items in this menu, will be empty if none
    {
        $options = $this->_normalizeOptions($options);

        // attempt to expand original container recursively.
        $expanded = new P4Cms_Navigation;
        try {
            $original = $this->getContainer();
            $this->_expandContainer($original, $expanded, $options);
        } catch (Exception $e) {
            P4Cms_Log::logException("Failed to get expanded menu.", $e);
            $expanded = new P4Cms_Navigation;
        }

        return $expanded;
    }
static P4Cms_Menu::getItemId ( item) [static]

Determine the unique identifier of the given menu item.

Returns null if no unique id can be determined.

For standard menu items, the id is taken from the UUID field. If a standard item has no UUID, returns null.

For items that result from dynamic expansion, the id is the combination of the dynamic item's UUID and the 'expansionId' field. If the expanded item has no expansionId, we return null. The expansionId can be provided by the dynamic handler during expansion.

Parameters:
Zend_Navigation_Page$itemitem to determine id of.
Returns:
string|null id of form uuid, or uuid/hex-encoded-expansion-id.
    {
        // detect dynamic expansion items.
        if ($item->dynamic instanceof P4Cms_Navigation_Page_Dynamic) {
            if (empty($item->expansionId)
                || !is_string($item->expansionId)
                || empty($item->dynamic->uuid)
            ) {
                return null;
            }

            return $item->dynamic->uuid . '/' . bin2hex($item->expansionId);
        }

        return empty($item->uuid) ? null : $item->uuid;
    }
P4Cms_Menu::getLabel ( )

Get the human friendly menu name.

If no explicit label has been set, the ID will be used to generate a default value.

Returns:
string Human friendly menu label
    {
        return $this->_getValue('label') ?: ucwords(
            str_replace('-', ' ', $this->getId())
        );
    }
static P4Cms_Menu::installDefaultMenus ( limit = null,
P4Cms_Record_Adapter adapter = null 
) [static]

Collect all of the default menus/items and install any that are missing.

Parameters:
string | null$limitoptional - limit install to the given menu id.
P4Cms_Record_Adapter$adapteroptional - storage adapter to use.
    {
        // clear the module/theme cache
        P4Cms_Module::clearCache();
        P4Cms_Theme::clearCache();

        // get all enabled modules.
        $packages = P4Cms_Module::fetchAllEnabled();

        // add current theme to packages
        if (P4Cms_Theme::hasActive()) {
            $packages[] = P4Cms_Theme::fetchActive();
        }

        // install default menus for each package.
        foreach ($packages as $package) {
            static::installPackageDefaults($package, $limit, $adapter);
        }
    }
static P4Cms_Menu::installPackageDefaults ( P4Cms_PackageAbstract package,
limit = null,
P4Cms_Record_Adapter adapter = null 
) [static]

Install the default menus contributed by a package.

Parameters:
P4Cms_PackageAbstract$packagethe package whose menu items will be installed
string | null$limitoptional - limit install to the given menu id.
P4Cms_Record_Adapter$adapteroptional - storage adapter to use.
    {
        // if no adapter given, use default.
        $adapter = $adapter ?: static::getDefaultAdapter();

        $menus = array();
        foreach ($package->getMenus() as $menuId => $entries) {

            // if limiting install to a single menu, only process matching menu.
            if ($limit && $menuId !== $limit) {
                continue;
            }

            // fetch or create the menu as appropriate.
            if (isset($menus[$menuId])) {
                $menu = $menus[$menuId];
            } else if (static::exists($menuId, null, $adapter)) {
                $menu = static::fetch($menuId, null, $adapter);
            } else {
                $menu = new static;
                $menu->setId($menuId)
                     ->setAdapter($adapter);
            }

            $menus[$menuId] = $menu;

            // add each entry to the menu.
            // if entry is an array, it must be a menu item or sub-menu.
            // otherwise, assume it's a menu property (e.g. label, order).
            foreach ($entries as $entryId => $entry) {
                if (is_array($entry)) {
                    $menu->addDefaultEntry($entry, $entryId);
                } else {
                    $menu->setValue($entryId, $entry);
                }
            }
        }

        // save menus.
        foreach ($menus as $menu) {
            $menu->save();
        }
    }
static P4Cms_Menu::isDynamicHandlerId ( id) [static]

Determine if the given id represents a dynamic handler id.

This is denoted by a dynamic handler class prefix. If the id is a dynamic handler id, returns the trailing handler id; otherwise false.

Parameters:
string$idthe id to examine
Returns:
string|bool the trailing handler id or false if not a handler
    {
        if (!preg_match('#P4Cms_Navigation_DynamicHandler/(.+)#', $id, $matches)) {
            return false;
        }

        return $matches[1];
    }
P4Cms_Menu::merge ( P4Cms_Menu theirs,
P4Cms_Menu base 
)

Merge the given menu into this menu.

Differences in their menu (with respect to the given base menu) are applied to our menu, unless the difference conflicts with a diff in our menu (also with respect to base).

Parameters:
P4Cms_Menu$theirsthe menu to merge to apply changes from.
P4Cms_Menu$basethe menu to diff ours and theirs against.
Returns:
P4Cms_Menu provides fluent interface
    {
        $container  = $this->getContainer();
        $ourDiffs   = $this->diff($base);
        $theirDiffs = $theirs->diff($base);

        foreach ($theirDiffs as $uuid => $diff) {
            // if this is a non-positional difference and the item is unchanged
            // or doesn't exist in our container we want to incorporate their diff
            // three distinct cases to handle here:
            //  a) insert (they added a new item)
            //  b) change (they modified an existing item)
            //  c) delete (they removed an existing item)
            if (!isset($ourDiffs[$uuid]) || $ourDiffs[$uuid]['type'] == 'same') {
                // a) just insert this item, its children will be handled later
                if ($diff['type'] == 'insert') {
                    $insert = clone $diff['right'];
                    $insert->setPages(array());

                    $container->addPage($insert);
                }

                // b) clobber our item with the modified item from theirs
                // but keep the sub-pages and position of our item.
                // if we can't find the item in our container we assume we
                // have deleted it; our delete trumps their edit
                if ($diff['type'] == 'change') {
                    $item = $container->findBy('uuid', $uuid);
                    if ($item) {
                        $updated = clone $diff['right'];
                        $updated->setPages($item->getPages());
                        $updated->set('order', $item->get('order'));

                        $parent = $item->getParent();
                        $parent->removePage($item);
                        $parent->addPage($updated);
                    }
                }

                // c) simply remove the item from our container.
                if ($diff['type'] == 'delete') {
                    $item = $container->findBy('uuid', $uuid);
                    if ($item) {
                        $item->getParent()->removePage($item);
                    }
                }
            }

            // if their diff is a move or insert and we don't have this item or our
            // diff is not a move, position the item within our container based on
            // its position in their container
            if (($diff['isMove'] || $diff['type'] == 'insert')
                && (!isset($ourDiffs[$uuid]) || !$ourDiffs[$uuid]['isMove'])
            ) {
                // if item cannot be located; nothing to move
                $item = $container->findBy('uuid', $uuid);
                if (!$item) {
                    continue;
                }

                // skip items whose parent cannot be located in our container
                $parent = $diff['right']->getParent();
                $parent = $parent instanceof Zend_Navigation_Page
                    ? $container->findBy('uuid', $parent->uuid)
                    : $container;
                if (!$parent) {
                    continue;
                }

                // ensure item is the correct place in our menu hierarchy
                $item->setParent($parent);

                // attempt to position item in the same place in our container
                // scan over siblings before this one in their container
                // and locate the first one that also exists in our container
                $found    = false;
                $previous = array();
                foreach ($diff['right']->getParent() as $sibling) {
                    if ($sibling->uuid == $uuid) {
                        break;
                    }
                    $previous[] = $sibling;
                }
                foreach (array_reverse($previous) as $sibling) {
                    foreach ($parent as $candidate) {
                        if ($candidate->uuid == $sibling->uuid) {
                            $found = $candidate;
                            break 2;
                        }
                    }
                }

                // update order of all items with this parent
                // if we could not find a suitable prior sibling
                // place this item first under this parent;
                // otherwise position after the found item.
                $order   = 0;
                $padding = P4Cms_Menu::ITEM_ORDER_PADDING;
                if (!$found) {
                    $item->order = ++$order * $padding;
                }
                foreach (iterator_to_array($parent) as $sibling) {
                    if ($sibling == $item) {
                        continue;
                    }
                    $sibling->order = ++$order * $padding;
                    if ($found && $sibling == $found) {
                        $item->order = ++$order * $padding;
                    }
                }
            }
        }

        return $this;
    }
P4Cms_Menu::recursiveCount ( Zend_Navigation_Container $  container)

Helper to count all of the items in a navigation container recursively.

Parameters:
Zend_Navigation_Container$containerthe container to count all of the items in.
Returns:
int the count of all items in container
    {
        $recursive = new RecursiveIteratorIterator(
            $container,
            RecursiveIteratorIterator::SELF_FIRST
        );

        // it appears we need to convert to an array
        // to count the items because count() on the
        // recursive iterator doesn't seem to work.
        return count(iterator_to_array($recursive));
    }
P4Cms_Menu::removeDefaultEntry ( array $  entry,
entryId,
Zend_Navigation_Container $  container = null 
)

Remove items introduced via addDefaultEntry().

Only removes items that still have their default values and have no sub-pages.

Parameters:
array$entrythe menu item definition
string$entryIdthe identifier for finding this item
Zend_Navigation_Container$containeroptional - container to remove item from
Returns:
P4Cms_Menu To maintain a fluent interface.
    {
        // source container defaults to top-level of menu
        $container = $container ?: $this->getContainer();

        // find the entry, nothing to do if we can't.
        // when we installed the item we generated an uuid from
        // the entry id, we do this again so we can find it.
        $uuid = P4Cms_Uuid::fromMd5(md5($entryId))->get();
        if (!$page = $container->findBy('uuid', $uuid)) {
            return $this;
        }

        // if the entry has sub-pages, remove them first.
        if (isset($entry['pages']) && is_array($entry['pages'])) {
            foreach ($entry['pages'] as $subEntryId => $subEntry) {
                $this->removeDefaultEntry(
                    $subEntry,
                    $entryId . "/" . $subEntryId,
                    $page
                );
            }
        }

        // attempt to turn the default item into an actual object then
        // translate it back to an array. running through the object
        // like this will often add additional derived or default values
        // to the array and allow our later matching logic to work.
        try {
            // remove any child pages if present; we are
            // only looking at this particular item.
            unset($entry['pages']);

            // instantiate the inferred type then to array it
            $entry = Zend_Navigation_Page::factory(
                P4Cms_Navigation::inferPageType($entry)
            )->toArray();
        } catch (Exception $e) {
            // simply eat any exceptions and chug
            // along with the existing values array
        }

        // remove the entry provided:
        //  - it has no sub-pages
        //  - it has not been modified (can be moved)
        $exclude = array('pages', 'type', 'typeInferred', 'order', 'uuid', 'visible');
        $default = $this->_getPageProperties($entry, $exclude);
        $current = $this->_getPageProperties($page,  $exclude);
        if (!$page->hasPages() && $current == $default) {
            $container->removePage($page);
        }

        return $this;
    }
static P4Cms_Menu::removePackageDefaults ( P4Cms_PackageAbstract package,
P4Cms_Record_Adapter adapter = null 
) [static]

Remove the default menus contributed by a package.

Parameters:
P4Cms_PackageAbstract$packagethe package whose menu items will be removed
P4Cms_Record_Adapter$adapteroptional - storage adapter to use.
    {
        // if no adapter given, use default.
        $adapter = $adapter ?: static::getDefaultAdapter();

        $menus    = array();
        foreach ($package->getMenus() as $menuId => $entries) {

            // fetch the menu if we haven't already done so.
            // skip this menu item if the menu doesn't exist.
            if (isset($menus[$menuId])) {
                $menu = $menus[$menuId];
            } else if (static::exists($menuId, null, $adapter)) {
                $menu = static::fetch($menuId, null, $adapter);
            } else {
                continue;
            }

            $menus[$menuId] = $menu;

            // remove each entry from the menu.
            foreach ($entries as $entryId => $entry) {
                if (is_array($entry)) {
                    $menu->removeDefaultEntry($entry, $entryId);
                }
            }
        }

        // save menus.
        foreach ($menus as $menu) {
            if ($menu->getContainer()->hasPages()) {
                $menu->save();
            } else {
                $menu->delete();
            }
        }
    }
P4Cms_Menu::save ( description = null)

Save this menu.

Extends parent to force UUIDs on menu items.

Parameters:
string$descriptionoptional - a description of the change.
Returns:
P4Cms_Record provides a fluent interface

Reimplemented from P4Cms_Record_Config.

    {
        // generate UUIDs to any items with missing or duplicate UUIDs
        $container = $this->getContainer();
        $recursive = new RecursiveIteratorIterator(
            $container,
            RecursiveIteratorIterator::SELF_FIRST
        );

        $uuids = array();
        foreach ($recursive as $item) {
            if (empty($item->uuid) || isset($uuids[$item->uuid])) {
                $item->uuid = (string) new P4Cms_Uuid;
            }
            $uuids[$item->uuid] = true;
        }

        // update config from instance container.
        if ($this->_container instanceof Zend_Navigation_Container) {
            $this->getConfig()->container = $container->toArray();
        }

        // let parent do the rest.
        parent::save($description);
    }
P4Cms_Menu::setContainer ( container)

Sets the raw Navigation Container.

Expects dynamic items to be unexpanded.

Parameters:
Zend_Navigation_Container | array | null$containerThe top level navigation container
Returns:
P4Cms_Menu Provides fluent interface
Exceptions:
InvalidArgumentExceptionIf passed $container is invalid type
    {
        if (!is_null($container)
            && !is_array($container)
            && !$container instanceof Zend_Navigation_Container
        ) {
            throw new InvalidArgumentException(
                "Cannot set container, expected Zend_Navigation_Container, array or null."
            );
        }

        $this->_container = $container instanceof Zend_Navigation_Container
            ? $container
            : new P4Cms_Navigation($container);

        return $this;
    }
P4Cms_Menu::setLabel ( label)

Set a human friendly menu name.

Parameters:
string | null$labelThe human friendly menu name to use
Returns:
P4Cms_Menu Provides fluent interface
    {
        return $this->_setValue('label', $label);
    }
P4Cms_Menu::trimContainer ( container,
maxDepth,
maxItems 
)

Trim the given navigation container according to the passed maximum depth and maximum items limits.

Returns the total number of items left in the container.

Parameters:
Zend_Navigation_Container$containera navigation container to trim.
int | null$maxDeptha maximum depth to permit before trimming.
int | null$maxItemsa maximum number of items to allow.
Returns:
int the total number of items left in the container.
    {
        $remove    = array();
        $itemCount = 0;
        $recursive = new RecursiveIteratorIterator(
            $container,
            RecursiveIteratorIterator::SELF_FIRST
        );

        // flag item for removal if max items or max depth exceeded.
        foreach ($recursive as $item) {
            if (($maxItems !== null && $itemCount >= $maxItems)
                || ($maxDepth !== null && $recursive->getDepth() > $maxDepth)
            ) {
                $remove[] = $item;
            } else {
                $itemCount++;
            }
        }

        // remove items flagged for removal.
        foreach ($remove as $item) {
            $item->getParent()->removePage($item);
        }

        return $itemCount;
    }

Member Data Documentation

P4Cms_Menu::$_container = null [protected]
P4Cms_Menu::$_fields [static, protected]
Initial value:
 array(
        'config'        => array(
            'accessor'  => 'getConfig',
            'mutator'   => 'setConfig'
        ),
        'label'         => array(
            'accessor'  => 'getLabel',
            'mutator'   => 'setLabel'
        )
    )

Specifies the array of fields that the current Record class wishes to use.

The implementing class MUST set this property.

Reimplemented from P4Cms_Record_Config.

P4Cms_Menu::$_handlers = null [static, protected]
P4Cms_Menu::$_storageSubPath = 'menus' [static, protected]

Specifies the sub-path to use for storage of records.

This is used in combination with the records path (provided by the storage adapter) to construct the full storage path. The implementing class MUST set this property.

Reimplemented from P4Cms_Record.

const P4Cms_Menu::DEFAULT_MENU = 'primary'
const P4Cms_Menu::MENU_KEEP_ROOT = 'keepRoot'
const P4Cms_Menu::MENU_MAX_DEPTH = 'maxDepth'
const P4Cms_Menu::MENU_MAX_ITEMS = 'maxItems'
const P4Cms_Menu::MENU_ROOT = 'root'

The documentation for this class was generated from the following file: