Upgrading
0.2 to 0.3
0.3 fixes several cases where the plugin did something other than documented. Most apps need no code changes, but check the points below.
May need a change
- Custom
ItemInterfaceandMenuInterfaceimplementations must match the widened signatures: labels, icons and badges acceptstring|Stringable;setIcon()andsetBadge()take abool $escape = trueargument;ItemInterfaceaddsshouldEscapeIcon()andshouldEscapeBadge(). Classes extendingItemorMenuneed no change. getBreadcrumbs()titles are HTML-escaped (unless the item disables label escaping) and honortranslate, because Cake'sBreadcrumbsHelperprints titles unescaped. If you print the returned titles yourself, drop your ownh()call to avoid double escaping.- Login, permission and authorization resolvers only hide items. A later resolver no longer shows an item hidden by another resolver, and an item built with
visible => falsestays hidden. - Duplicate ids and explicit keys are rejected across the whole tree, not only within one submenu.
fromFlat()rejects duplicate keys before importing anything. getCurrentItem()returns the item from a resolved copy of the menu, not the menu's own item instance. Its state and ancestors are intact, but changes to it do not affect the menu.- Bootstrap 5 puts
activeon the link or dropdown toggle instead of the<li>. SetactiveOn => 'both'to keep it on both. aria-expandedmoves from the<li>to the link or label that toggles the submenu. Withroles => true, Bootstrap 5 and sidebar toggles userole="menuitem"instead ofbutton.
Behaves as documented now
getCurrentItem()and breadcrumbs return the deepest active item, and skip hidden items and everything below them.- Visibility you resolve on a menu before calling the helper survives
render(). Runtime active and expanded state is cleared before the helper resolves the request, then restored afterward. - Links with a custom route
_namebecome active. String URLs match the request's app-relative path. If a link starts with the request's base path, the base is stripped before matching. Trailing slashes and percent-encoded characters are normalized, except encoded slashes. newItem()/addItem()applylinkAttributes,externalandexpanded, and key lookup falls back to the label slug.linkCurrent => truelinks the last crumb inrenderBreadcrumbs(), and only that crumb getsaria-current="page".- Empty nested lists are no longer rendered, and
firstClass/lastClasscount only rendered items.
Other changes
- Menu cache keys include the locale; existing entries are rebuilt once per locale.
- Icons and badges stay escaped by default. Pass
escapeIcon => false/escapeBadge => false(orsetIcon($icon, false)) for trusted markup such as icon font tags. freeze()also freezes the item's link, andtoArray()/fromArray()round trips are lossless.
0.1 to 0.2
0.2 renames the PHP namespace and the plugin from Menu to CakeMenu. A bare Menu namespace collides easily with application code and other packages, so the plugin now owns a distinct one. The class names themselves (Menu, MenuHelper, Item, the resolvers and renderers) are unchanged.
Namespace
Replace the Menu\ prefix with CakeMenu\ in every use statement and fully qualified name:
use Menu\Menu;
use Menu\Resolver\UrlArrayResolver;
use CakeMenu\Menu;
use CakeMenu\Resolver\UrlArrayResolver; A project-wide search and replace of Menu\ to CakeMenu\ covers most apps. Check custom resolvers and renderers that implement the plugin's interfaces, and docblock types such as \Menu\Item\ItemInterface.
Plugin and helper
The plugin name is now CakeMenu, and the plugin class is CakeMenu\CakeMenuPlugin:
// src/Application.php
$this->addPlugin('Menu');
$this->addPlugin('CakeMenu'); // src/View/AppView.php
$this->addHelper('Menu.Menu');
$this->addHelper('CakeMenu.Menu'); The helper keeps its Menu alias, so $this->Menu->render('main') in templates stays as it is. If you load plugins through config/plugins.php, rename the 'Menu' key there.
Configuration key
Menus declared in configuration move from Menu.menus to CakeMenu.menus:
return [
'Menu' => [
'CakeMenu' => [
'menus' => [
'main' => [/* ... */],
],
],
];The old Menu.menus key is still read when CakeMenu.menus is not set, and triggers a deprecation warning. Support for it will be removed in a later release.
Spec files created earlier with bin/cake menu generate use the old key. Rename it there too.
Resolver interface
ContextAwareResolverInterface is removed. Every resolver implements ResolverInterface with a required context. Rename resolveWithContext() and remove any wrapper resolve():
public function resolveWithContext(ItemInterface $item, ResolverContext $context): void
public function resolve(ItemInterface $item, ResolverContext $context): voidDirect calls must pass a context; Menu::resolve() creates it for you. ResolverCollectionInterface now extends ResolverInterface, so replace ResolverInterface|ResolverCollectionInterface types with ResolverInterface.
Runtime state
StateResetInterface and RuntimeStateTrait are removed. Custom items implement resetState(), setRuntimeVisible(), setRuntimeActive(), and setRuntimeExpanded() as part of ItemInterface. Rename setRuntimeVisibility() to setRuntimeVisible(). Resolvers call these setters directly:
$item->setActive(true);
$item->setRuntimeActive(true);
$item->setExpanded();
$item->setRuntimeExpanded(); Authoring setters change defaults only and throw on a frozen item. Runtime setters override the defaults until resetState() and stay available on frozen menus. Custom items also implement getRuntimeState(), which returns the raw overrides (null when unset).
Visibility and matching
Rename setVisibility() to setVisible(); isVisible() is unchanged. Replace setFuzzyMatch(), isFuzzyMatch(), and getFuzzyMatchSetting() with setFuzzy(?bool) and getFuzzy(): ?bool:
$item->setVisibility(false)->setFuzzyMatch();
$item->setVisible(false)->setFuzzy(true); null inherits the resolver setting. The fuzzy option key stays the same. URL resolvers now use ItemInterface matching methods for custom items too. setParent(), setOwnerMenu(), and MenuInterface::setOwnerItem() are internal tree plumbing.
Item paths
Custom items must implement getPath() (root-to-self, including self), getLevel() (zero for top-level items), and getRoot() (the top-level ancestor). MenuHelper::extractPath() delegates to getPath() and no longer accepts options:
$path = $this->Menu->extractPath($item, $options);
$path = $item->getPath(); Serialization
Item::toArray() now writes authoring defaults for visible, active, and expanded. Rebuilding a resolved menu with Menu::fromArray() no longer preserves request-specific state. Apply resolvers again for the current request.
Helper build options
Replace menuAttributes with attributes:
$this->Menu->create('main', ['menuAttributes' => ['class' => 'nav']]);
$this->Menu->create('main', ['attributes' => ['class' => 'nav']]); attributes, overwrite, cache, and rebuild are build options. Renderers no longer receive them as per-menu defaults. Other options still become render defaults.
Breadcrumb rendering
renderBreadcrumbs() takes only the menu and options, and always uses BreadcrumbRenderer. Its renderer option accepts a subclass name or instance. For Cake's helper markup:
$this->Menu->renderBreadcrumbs('main', [], $attributes, $separator);
$this->Menu->populateBreadcrumbs('main');
$this->Breadcrumbs->render($attributes, $separator); Callback and authorizer arguments
Registration closures always receive ($menu, $helper). Closures may omit unused parameters. PermissionResolver always calls can($identity, $permission, $item); adapt authorizers that previously expected another argument order or a resolver context.
public function can(string $permission): bool
public function can(mixed $identity, string $permission, ItemInterface $item): boolSort direction
Replace string directions and the removed Menu::SORT_ASC / SORT_DESC constants with CakeMenu\SortDirection:
$menu->sortBy('weight', Menu::SORT_DESC);
$menu->sortBy('weight', SortDirection::Desc); SortDirection::Asc remains the default. Custom menus must update their method signature.
Authentication state
LoggedInResolver accepts CakeMenu\Resolver\AuthState in item data:
$item->setData('auth', 'loggedIn');
$item->setData('auth', AuthState::LoggedIn); The backed strings 'loggedIn' and 'loggedOut' still work in array configuration. Unknown strings now throw InvalidArgumentException instead of being ignored.
Closure signatures
Menu filter/find/sort callbacks, helper registration callbacks, CallbackResolver, and AuthorizationResolver now require Closure. Convert function names and method arrays with first-class callable syntax:
$menu->filter([$service, 'isVisible']);
$menu->filter($service->isVisible(...)); Custom implementations must replace callable parameter types with Closure.
Custom menu interfaces
Custom MenuInterface implementations must add collect(): ItemCollection, setItemClass(string $class): static, and getItemClass(): string. Menu::setItemClass() validates that the class implements ItemInterface. New item submenus inherit the owning menu's item class instead of always using Item. Set the class before building the tree.
Regex request matching
Pass a ServerRequestInterface instead of a path string. Move the custom data key into the options array:
new RegexResolver($request->getUri()->getPath(), 'activePattern');
new RegexResolver($request, ['dataKey' => 'activePattern', 'maxDepth' => 2]); The resolver matches the URI path. maxDepth uses ResolverContext depth (top level is 1), like the other request resolvers; null leaves depth unlimited.