Completed
Push — master ( bbb282...43d0b8 )
by Daniel
25s
created

CMSMenu   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 4
Bugs 0 Features 1
Metric Value
wmc 46
c 4
b 0
f 1
lcom 1
cbo 10
dl 0
loc 359
rs 8.3999

16 Methods

Rating   Name   Duplication   Size   Complexity  
A populate_menu() 0 3 1
A add_controller() 0 5 2
A menuitem_for_controller() 0 19 2
A add_link() 0 3 1
A add_menu_item() 0 9 2
A get_menu_item() 0 4 2
A get_menu_code() 0 3 1
D get_menu_items() 0 42 9
C get_viewable_menu_items() 0 25 8
A remove_menu_item() 0 3 1
A remove_menu_class() 0 4 1
A clear_menu() 0 4 1
A replace_menu_item() 0 14 2
A add_menu_item_obj() 0 7 1
D get_cms_classes() 0 35 9
A getIterator() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like CMSMenu often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CMSMenu, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Admin;
4
5
6
use SilverStripe\Security\Member;
7
use Object;
8
use IteratorAggregate;
9
use i18nEntityProvider;
10
use Config;
11
use Controller;
12
use Convert;
13
use ClassInfo;
14
use ReflectionClass;
15
use ArrayIterator;
16
use i18n;
17
18
/**
19
 * The object manages the main CMS menu. See {@link LeftAndMain::init()} for
20
 * example usage.
21
 *
22
 * The menu will be automatically populated with menu items for subclasses of
23
 * {@link LeftAndMain}. That is, for each class in the CMS that creates an
24
 * administration panel, a CMS menu item will be created. The default
25
 * configuration will also include a 'help' link to the SilverStripe user
26
 * documentation.
27
 *
28
 * Additional CMSMenu items can be added through {@link LeftAndMainExtension::init()}
29
 * extensions added to {@link LeftAndMain}.
30
 *
31
 * @package framework
32
 * @subpackage admin
33
 */
34
class
35
CMSMenu extends Object implements IteratorAggregate, i18nEntityProvider {
0 ignored issues
show
Coding Style introduced by
The extends keyword must be on the same line as the class name
Loading history...
Coding Style introduced by
The implements keyword must be on the same line as the class name
Loading history...
36
37
	/**
38
	 * Sort by menu priority, highest to lowest
39
	 */
40
	const MENU_PRIORITY = 'menu_priority';
41
42
	/**
43
	 * Sort by url priority, highest to lowest
44
	 */
45
	const URL_PRIORITY = 'url_priority';
46
47
	/**
48
	 * An array of changes to be made to the menu items, in the order that the changes should be
49
	 * applied.  Each item is a map in one of the two forms:
50
	 *  - array('type' => 'add', 'item' => new CMSMenuItem(...) )
51
	 *  - array('type' => 'remove', 'code' => 'codename' )
52
	 */
53
	protected static $menu_item_changes = array();
54
55
	/**
56
	 * Set to true if clear_menu() is called, to indicate that the default menu shouldn't be
57
	 * included
58
	 */
59
	protected static $menu_is_cleared = false;
60
61
	/**
62
	 * Generate CMS main menu items by collecting valid
63
	 * subclasses of {@link LeftAndMain}
64
	 */
65
	public static function populate_menu() {
66
		self::$menu_is_cleared = false;
67
	}
68
69
	/**
70
	 * Add a LeftAndMain controller to the CMS menu.
71
	 *
72
	 * @param string $controllerClass The class name of the controller
73
	 * @todo A director rule is added when a controller link is added, but it won't be removed
74
	 *			when the item is removed. Functionality needed in {@link Director}.
75
	 */
76
	public static function add_controller($controllerClass) {
77
		if($menuItem = self::menuitem_for_controller($controllerClass)) {
78
			self::add_menu_item_obj($controllerClass, $menuItem);
79
		}
80
	}
81
82
	/**
83
	 * Return a CMSMenuItem to add the given controller to the CMSMenu
84
	 *
85
	 * @param string $controllerClass
86
	 * @return CMSMenuItem
87
	 */
88
	protected static function menuitem_for_controller($controllerClass) {
89
		$urlBase = AdminRootController::admin_url();
90
		$urlSegment   = Config::inst()->get($controllerClass, 'url_segment', Config::FIRST_SET);
91
		$menuPriority = Config::inst()->get($controllerClass, 'menu_priority', Config::FIRST_SET);
92
93
		// Don't add menu items defined the old way
94
		if (!$urlSegment) {
95
			return null;
96
		}
97
98
		$link = Controller::join_links($urlBase, $urlSegment) . '/';
99
100
		// doesn't work if called outside of a controller context (e.g. in _config.php)
101
		// as the locale won't be detected properly. Use {@link LeftAndMain->MainMenu()} to update
102
		// titles for existing menu entries
103
		$menuTitle = LeftAndMain::menu_title($controllerClass);
104
105
		return new CMSMenuItem($menuTitle, $link, $controllerClass, $menuPriority);
106
	}
107
108
109
	/**
110
	 * Add an arbitrary URL to the CMS menu.
111
	 *
112
	 * @param string $code A unique identifier (used to create a CSS ID and its key in {@link $menu_items})
113
	 * @param string $menuTitle The link's title in the CMS menu
114
	 * @param string $url The url of the link
115
	 * @param integer $priority The menu priority (sorting order) of the menu item.  Higher priorities will be further
116
	 *                          left.
117
	 * @param array $attributes an array of attributes to include on the link.
118
	 *
119
	 * @return boolean The result of the operation.
120
	 */
121
	public static function add_link($code, $menuTitle, $url, $priority = -1, $attributes = null) {
122
		return self::add_menu_item($code, $menuTitle, $url, null, $priority, $attributes);
123
	}
124
125
	/**
126
	 * Add a navigation item to the main administration menu showing in the top bar.
127
	 *
128
	 * uses {@link CMSMenu::$menu_items}
129
	 *
130
	 * @param string $code Unique identifier for this menu item (e.g. used by {@link replace_menu_item()} and
131
	 *                    {@link remove_menu_item}. Also used as a CSS-class for icon customization.
132
	 * @param string $menuTitle Localized title showing in the menu bar
133
	 * @param string $url A relative URL that will be linked in the menu bar.
134
	 * @param string $controllerClass The controller class for this menu, used to check permisssions.
135
	 *                    If blank, it's assumed that this is public, and always shown to users who
136
	 *                    have the rights to access some other part of the admin area.
137
	 * @param int $priority
138
	 * @param array $attributes an array of attributes to include on the link.
139
	 * @return bool Success
140
	 */
141
	public static function add_menu_item($code, $menuTitle, $url, $controllerClass = null, $priority = -1,
142
											$attributes = null) {
143
		// If a class is defined, then force the use of that as a code.  This helps prevent menu item duplication
144
		if($controllerClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $controllerClass of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
145
			$code = self::get_menu_code($controllerClass);
146
		}
147
148
		return self::replace_menu_item($code, $menuTitle, $url, $controllerClass, $priority, $attributes);
0 ignored issues
show
Bug introduced by
It seems like $code defined by self::get_menu_code($controllerClass) on line 145 can also be of type array; however, SilverStripe\Admin\CMSMenu::replace_menu_item() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
149
	}
150
151
	/**
152
	 * Get a single menu item by its code value.
153
	 *
154
	 * @param string $code
155
	 * @return array
156
	 */
157
	public static function get_menu_item($code) {
158
		$menuItems = self::get_menu_items();
159
		return (isset($menuItems[$code])) ? $menuItems[$code] : false;
160
	}
161
162
	/**
163
	 * Get menu code for class
164
	 *
165
	 * @param string $cmsClass Controller class name
166
	 * @return string
167
	 */
168
	public static function get_menu_code($cmsClass) {
169
		return Convert::raw2htmlname(str_replace('\\', '-', $cmsClass));
170
	}
171
172
	/**
173
	 * Get all menu entries.
174
	 *
175
	 * @return array
176
	 */
177
	public static function get_menu_items() {
178
		$menuItems = array();
179
180
		// Set up default menu items
181
		if(!self::$menu_is_cleared) {
182
			$cmsClasses = self::get_cms_classes();
183
			foreach($cmsClasses as $cmsClass) {
184
				$menuItem = self::menuitem_for_controller($cmsClass);
185
				$menuCode = self::get_menu_code($cmsClass);
186
				if($menuItem) {
187
					$menuItems[$menuCode] = $menuItem;
188
				}
189
			}
190
		}
191
192
		// Apply changes
193
		foreach(self::$menu_item_changes as $change) {
194
			switch($change['type']) {
195
				case 'add':
196
					$menuItems[$change['code']] = $change['item'];
197
					break;
198
199
				case 'remove':
200
					unset($menuItems[$change['code']]);
201
					break;
202
203
				default:
204
					user_error("Bad menu item change type {$change['type']}", E_USER_WARNING);
205
			}
206
		}
207
208
		// Sort menu items according to priority, then title asc
209
		$menuPriority = array();
210
		$menuTitle    = array();
211
		foreach($menuItems as $key => $menuItem) {
212
			$menuPriority[$key] = is_numeric($menuItem->priority) ? $menuItem->priority : 0;
213
			$menuTitle[$key]    = $menuItem->title;
214
		}
215
		array_multisort($menuPriority, SORT_DESC, $menuTitle, SORT_ASC, $menuItems);
216
217
		return $menuItems;
218
	}
219
220
	/**
221
	 * Get all menu items that the passed member can view.
222
	 * Defaults to {@link Member::currentUser()}.
223
	 *
224
	 * @param Member $member
225
	 * @return array
226
	 */
227
	public static function get_viewable_menu_items($member = null) {
228
		if(!$member && $member !== FALSE) {
229
			$member = Member::currentUser();
230
		}
231
232
		$viewableMenuItems = array();
233
		$allMenuItems = self::get_menu_items();
234
		if($allMenuItems) foreach($allMenuItems as $code => $menuItem) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allMenuItems of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
235
			// exclude all items which have a controller to perform permission
236
			// checks on
237
			if($menuItem->controller) {
238
				$controllerObj = singleton($menuItem->controller);
239
				if(Controller::has_curr()) {
240
					// Necessary for canView() to have request data available,
241
					// e.g. to check permissions against LeftAndMain->currentPage()
242
					$controllerObj->setRequest(Controller::curr()->getRequest());
243
					if(!$controllerObj->canView($member)) continue;
244
				}
245
			}
246
247
			$viewableMenuItems[$code] = $menuItem;
248
		}
249
250
		return $viewableMenuItems;
251
	}
252
253
	/**
254
	 * Removes an existing item from the menu.
255
	 *
256
	 * @param string $code Unique identifier for this menu item
257
	 */
258
	public static function remove_menu_item($code) {
259
		self::$menu_item_changes[] = array('type' => 'remove', 'code' => $code);
260
	}
261
262
	/**
263
	 * Remove menu item by class name.
264
	 *
265
	 * @param string $className Name of class
266
	 */
267
	public static function remove_menu_class($className) {
268
		$code = self::get_menu_code($className);
269
		self::remove_menu_item($code);
0 ignored issues
show
Bug introduced by
It seems like $code defined by self::get_menu_code($className) on line 268 can also be of type array; however, SilverStripe\Admin\CMSMenu::remove_menu_item() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
270
	}
271
272
	/**
273
	 * Clears the entire menu
274
	 */
275
	public static function clear_menu() {
276
		self::$menu_item_changes = array();
277
		self::$menu_is_cleared = true;
278
	}
279
280
	/**
281
	 * Replace a navigation item to the main administration menu showing in the top bar.
282
	 *
283
	 * @param string $code Unique identifier for this menu item (e.g. used by {@link replace_menu_item()} and
284
	 *                    {@link remove_menu_item}. Also used as a CSS-class for icon customization.
285
	 * @param string $menuTitle Localized title showing in the menu bar
286
	 * @param string $url A relative URL that will be linked in the menu bar.
287
	 *                    Make sure to add a matching route via {@link Director::$rules} to this url.
288
	 * @param string $controllerClass The controller class for this menu, used to check permisssions.
289
	 *                    If blank, it's assumed that this is public, and always shown to users who
290
	 *                    have the rights to access some other part of the admin area.
291
	 * @param int $priority
292
	 * @param array $attributes an array of attributes to include on the link.
293
	 * @return bool Success
294
	 */
295
	public static function replace_menu_item($code, $menuTitle, $url, $controllerClass = null, $priority = -1,
296
												$attributes = null) {
297
		$item = new CMSMenuItem($menuTitle, $url, $controllerClass, $priority);
298
299
		if($attributes) {
300
			$item->setAttributes($attributes);
301
		}
302
303
		self::$menu_item_changes[] = array(
304
			'type' => 'add',
305
			'code' => $code,
306
			'item' => $item,
307
		);
308
	}
309
310
	/**
311
	 * Add a previously built menu item object to the menu
312
	 *
313
	 * @param string $code
314
	 * @param CMSMenuItem $cmsMenuItem
315
	 */
316
	protected static function add_menu_item_obj($code, $cmsMenuItem) {
317
		self::$menu_item_changes[] = array(
318
			'type' => 'add',
319
			'code' => $code,
320
			'item' => $cmsMenuItem,
321
		);
322
	}
323
324
	/**
325
	 * A utility funciton to retrieve subclasses of a given class that
326
	 * are instantiable (ie, not abstract) and have a valid menu title.
327
	 *
328
	 * Sorted by url_priority config.
329
	 *
330
	 * @todo A variation of this function could probably be moved to {@link ClassInfo}
331
	 * @param string $root The root class to begin finding subclasses
332
	 * @param boolean $recursive Look for subclasses recursively?
333
	 * @param string $sort Name of config on which to sort. Can be 'menu_priority' or 'url_priority'
334
	 * @return array Valid, unique subclasses
335
	 */
336
	public static function get_cms_classes($root = null, $recursive = true, $sort = self::MENU_PRIORITY) {
337
		if(!$root) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $root of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
338
			$root = 'SilverStripe\\Admin\\LeftAndMain';
339
		}
340
		/** @todo Make these actual abstract classes */
341
		$abstractClasses = ['SilverStripe\\Admin\\LeftAndMain', 'SilverStripe\\CMS\\Controllers\\CMSMain'];
342
		$subClasses = array_values(ClassInfo::subclassesFor($root));
343
		foreach($subClasses as $className) {
344
			if($recursive && $className != $root) {
345
				$subClasses = array_merge($subClasses, array_values(ClassInfo::subclassesFor($className)));
346
			}
347
		}
348
		$subClasses = array_unique($subClasses);
349
		foreach($subClasses as $key => $className) {
350
			// Remove abstract classes and LeftAndMain
351
			if(in_array($className, $abstractClasses) || ClassInfo::classImplements($className, 'TestOnly')) {
352
				unset($subClasses[$key]);
353
			} else {
354
				// Separate conditional to avoid autoloading the class
355
				$classReflection = new ReflectionClass($className);
356
				if(!$classReflection->isInstantiable()) {
357
					unset($subClasses[$key]);
358
				}
359
			}
360
		}
361
362
		// Sort by specified sorting config
363
		usort($subClasses, function ($a, $b) use ($sort) {
364
			$priorityA = Config::inst()->get($a, $sort);
365
			$priorityB = Config::inst()->get($b, $sort);
366
			return $priorityB - $priorityA;
367
		});
368
369
		return $subClasses;
370
	}
371
372
	/**
373
	 * IteratorAggregate Interface Method.  Iterates over the menu items.
374
	 */
375
	public function getIterator() {
376
		return new ArrayIterator(self::get_menu_items());
377
	}
378
379
	/**
380
	 * Provide menu titles to the i18n entity provider
381
	 */
382
	public function provideI18nEntities() {
383
		$cmsClasses = self::get_cms_classes();
384
		$entities = array();
385
		foreach($cmsClasses as $cmsClass) {
386
			$defaultTitle = LeftAndMain::menu_title($cmsClass, false);
387
			$ownerModule = i18n::get_owner_module($cmsClass);
388
			$entities["{$cmsClass}.MENUTITLE"] = array($defaultTitle, 'Menu title', $ownerModule);
389
		}
390
		return $entities;
391
	}
392
}
393