Permalink
Browse files

feature(menus): adds menu service for more orderly menu construction

A dedicated menu service provides tools for constructing menus such that
hooks and other steps are not forgotten, and not performed more than once.

`elgg()->menus->getMenu` builds a complete menu with all hooks pre-triggered,
or `getUnpreparedMenu` can just return a linear menu before the items have been
split into sections and run through the `prepare` hook.

In either case, passing these objects to `elgg_view_menu()` completes the
preparation process before passing the menu to a view.

Fixes #9508
  • Loading branch information...
mrclay committed Mar 20, 2016
1 parent b96c736 commit 38ecfc6ba9b3aa431e9b553651e26c3dd10bbc18
@@ -83,13 +83,16 @@ System hooks
In ``elgg_view_layout()``, filters the return value of the layout view.
**parameters, menu:<menu_name>**
Triggered by ``elgg_view_menu()``. Used to change menu variables (like sort order) before it is generated.
Triggered by ``elgg_view_menu()``. Used to change menu variables (like sort order) before rendering.
**register, menu:<menu_name>**
Triggered by ``elgg_view_menu()``. Used to add dynamic menu items.
Filters the initial list of menu items pulled from configuration, before the menu has been split into
sections. Triggered by ``elgg_view_menu()`` and ``elgg()->menus->getMenu()``.
**prepare, menu:<menu_name>**
Trigger by ``elgg_view_menu()``. Used to sort, add, remove, and modify menu items.
Filters the array of menu sections before they're displayed. Each section is a string key mapping to
an area of menu items. This is a good hook to sort, add, remove, and modify menu items. Triggered by
``elgg_view_menu()`` and ``elgg()->menus->prepareMenu()``.
**creating, river**
Triggered before a river item is created. Return false to prevent river item from being created.
View
@@ -79,7 +79,7 @@ Examples
/**
* Change the URL of the "Albums" menu item in the owner_block menu
*/
function my_owner_block_menu_handler($hook, $type, $menu, $params) {
function my_owner_block_menu_handler($hook, $type, $items, $params) {
$owner = $params['entity'];
// Owner can be either user or a group, so we
@@ -93,15 +93,15 @@ Examples
break;
}
foreach ($menu as $key => $item) {
foreach ($items as $key => $item) {
if ($item->getName() == 'albums') {
// Set the new URL
$item->setURL($url);
break;
}
}
return $menu;
return $items;
}
**Example 2:** Modify the ``entity`` menu for the ``ElggBlog`` objects
@@ -121,7 +121,7 @@ Examples
/**
* Customize the entity menu for ElggBlog objects
*/
function my_entity_menu_handler($hook, $type, $menu, $params) {
function my_entity_menu_handler($hook, $type, $items, $params) {
// The entity can be found from the $params parameter
$entity = $params['entity'];
@@ -131,11 +131,11 @@ Examples
return $menu;
}
foreach ($menu as $key => $item) {
foreach ($items as $key => $item) {
switch ($item->getName()) {
case 'likes':
// Remove the "likes" menu item
unset($menu[$key]);
unset($items[$key]);
break;
case 'edit':
// Change the "Edit" text into a custom icon
@@ -144,7 +144,7 @@ Examples
}
}
return $menu;
return $items;
}
Creating a new menu
@@ -163,12 +163,14 @@ in alphapetical order:
.. code-block:: php
// in a resource view
echo elgg_view_menu('my_menu', array('sort_by' => 'title'));
You can now add new items to the menu like this:
.. code-block:: php
// in plugin init
elgg_register_menu_item('my_menu', array(
'name' => 'my_page',
'href' => 'path/to/my_page',
View
@@ -7,3 +7,9 @@ class will offer a set of service objects for plugins to use.
.. note::
If you have a useful idea, you can :doc:`add a new service </contribute/services>`!
Menus
-----
``elgg()->menus`` provides low-level methods for constructing menus. In general, menus should be
passed to ``elgg_view_menu`` for rendering instead of manual rendering.
@@ -14,6 +14,8 @@
* The full path is necessary to work around this: https://bugs.php.net/bug.php?id=55726
*
* @since 2.0.0
*
* @property-read \Elgg\Menu\Service $menus
*/
class Application {
@@ -41,6 +43,7 @@ class Application {
*/
private static $public_services = [
//'config' => true,
'menus' => true,
];
/**
@@ -39,6 +39,7 @@
* @property-read \Elgg\Http\Input $input
* @property-read \Elgg\Logger $logger
* @property-read Mailer $mailer
* @property-read \Elgg\Menu\Service $menus
* @property-read \Elgg\Cache\MetadataCache $metadataCache
* @property-read \Elgg\Database\MetadataTable $metadataTable
* @property-read \Elgg\Database\MetastringsTable $metastringsTable
@@ -192,6 +193,10 @@ public function __construct(\Elgg\Config $config) {
// TODO(evan): Support configurable transports...
$this->setClassName('mailer', 'Zend\Mail\Transport\Sendmail');
$this->setFactory('menus', function(ServiceProvider $c) {
return new \Elgg\Menu\Service($c->hooks, $c->config);
});
$this->setFactory('metadataCache', function (ServiceProvider $c) {
return new \Elgg\Cache\MetadataCache($c->session);
});
@@ -0,0 +1,69 @@
<?php
namespace Elgg\Menu;
use ElggMenuItem;
/**
* A complete menu, sorted, filtered by the "prepare" hook, and split into sections.
*
* This also encapsulates parameters to be passed to views.
*/
class Menu {
/**
* @var array
*/
private $params;
/**
* Constructor
*
* @param array $params Params. Must include:
* "name" menu name
* "menu" array of sections (each an array of items)
* @access private
* @internal Do not use. Use the `elgg()->menus` service methods instead.
*/
public function __construct(array $params) {
$this->params = $params;
}
/**
* Get all menu sections
*
* @return ElggMenuItem[][]
*/
public function getSections() {
return $this->params['menu'];
}
/**
* Get a single menu section
*
* @param string $name Section name
* @param mixed $default Value to return if section is not found
*
* @return ElggMenuItem[]|null
*/
public function getSection($name, $default = null) {
return isset($this->params['menu'][$name]) ? $this->params['menu'][$name] : $default;
}
/**
* Get the menu's name
*
* @return string
*/
public function getName() {
return $this->params['name'];
}
/**
* Get the menu parameters
*
* @return array
*/
public function getParams() {
return $this->params;
}
}
@@ -0,0 +1,134 @@
<?php
namespace Elgg\Menu;
use Elgg\PluginHooksService;
use Elgg\Config;
use ElggMenuBuilder;
/**
* Methods to construct and prepare menus for rendering
*/
class Service {
/**
* @var PluginHooksService
*/
private $hooks;
/**
* @var Config
*/
private $config;
/**
* Constructor
*
* @param PluginHooksService $hooks Plugin hooks
* @param Config $config Elgg config
* @access private
* @internal Do not use. Use `elgg()->menus`.
*/
public function __construct(PluginHooksService $hooks, Config $config) {
$this->hooks = $hooks;
$this->config = $config;
}
/**
* Build a full menu, pulling items from configuration and the "register" menu hooks.
*
* Parameters are filtered by the "parameters" hook.
*
* @param string $name Menu name
* @param array $params Hook/view parameters
*
* @return Menu
*/
public function getMenu($name, array $params = []) {
return $this->prepareMenu($this->getUnpreparedMenu($name, $params));
}
/**
* Build an unprepared menu.
*
* @param string $name Menu name
* @param array $params Hook/view parameters
*
* @return UnpreparedMenu
*/
public function getUnpreparedMenu($name, array $params = []) {
$menus = $this->config->getVolatile('menus');
$items = [];
if ($menus && isset($menus[$name])) {
$items = elgg_extract($name, $menus, []);
}
$params['name'] = $name;
$params = $this->hooks->trigger('parameters', "menu:$name", $params, $params);
if (!isset($params['sort_by'])) {
$params['sort_by'] = 'priority';

This comment has been minimized.

Show comment
Hide comment
@mrclay

mrclay Jan 30, 2017

Member

Re #10737, I think should be "text"

@mrclay

mrclay Jan 30, 2017

Member

Re #10737, I think should be "text"

}
$items = $this->hooks->trigger('register', "menu:$name", $params, $items);
return new UnpreparedMenu($params, $items);
}
/**
* Split a menu into sections, and pass it through the "prepare" hook
*
* @param UnpreparedMenu $menu Menu
*
* @return Menu
*/
public function prepareMenu(UnpreparedMenu $menu) {
$name = $menu->getName();
$params = $menu->getParams();
$sort_by = $menu->getSortBy();
$builder = new ElggMenuBuilder($menu->getItems());
$params['menu'] = $builder->getMenu($sort_by);
$params['selected_item'] = $builder->getSelected();
$params['menu'] = $this->hooks->trigger('prepare', "menu:$name", $params, $params['menu']);
return new Menu($params);
}
/**
* Combine several menus into one
*
* Unprepared menus will be built separately, then combined, with items reassigned to sections
* named after their origin menu. The returned menu must be prepared before display.
*
* @param string[] $names Menu names
* @param array $params Menu params
* @param string $new_name Combined menu name (used for the prepare hook)
*
* @return UnpreparedMenu
*/
function combineMenus(array $names = [], array $params = [], $new_name = '') {
if (!$new_name) {
$new_name = implode('__' , $names);
}
$all_items = [];
foreach ($names as $name) {
$items = $this->getMenu($name, $params)->getItems();
foreach ($items as $item) {
$section = $item->getSection();
if ($section == 'default') {
$item->setSection($name);
}
$item->setData('menu_name', $name);
$all_items[] = $item;
}
}
$params['name'] = $new_name;
return new UnpreparedMenu($params, $all_items);
}
}
Oops, something went wrong.

0 comments on commit 38ecfc6

Please sign in to comment.